Pattern Architetturali Sostenibili: Storage, Cache e Batch
Le decisioni architetturali che prendiamo ogni giorno - come strutturiamo lo storage, come gestiamo la cache, come orchestriamo i batch job - determinano il carbon footprint del software in misura 10-100 volte superiore rispetto alle micro-ottimizzazioni del codice. Un'architettura mal progettata che accede al database ad ogni richiesta, non usa il caching, e processa i dati in tempo reale quando potrebbe farlo in batch, può consumare ordini di grandezza più energia di un'architettura ben progettata.
Secondo un'analisi del Green Software Foundation Impact Framework, le scelte architetturali relative a storage tiering, caching e scheduling batch rappresentano il 40-60% del potenziale di riduzione delle emissioni di un sistema software enterprise. Non si tratta solo di ottimizzazione tecnica: è una responsabilità professionale che ogni architetto e sviluppatore deve considerare parte integrante del proprio mestiere.
In questo nono articolo della serie Green Software esploreremo i pattern architetturali sostenibili più efficaci: dallo storage tiering intelligente alla cache carbon-aware, dal batch scheduling nelle finestre di energia verde all'API design sostenibile. Con esempi di codice reali e un caso studio completo che dimostra una riduzione del 45% del carbon footprint di un e-commerce da 1 milione di visite giornaliere.
Cosa Imparerai
- Tiered storage (hot/warm/cold/archive) e politiche di data lifecycle per ridurre l'energia
- Pattern di caching carbon-aware: CDN geo-intelligente, multi-level caching, invalidation ottimizzata
- Batch processing nelle finestre di energia verde con Carbon Aware SDK
- Right-sizing e auto-scaling down: come evitare il "idle computing" che spreca energia
- Database pattern sostenibili: query optimization, materialized views, read replicas
- Network efficiency: compressione, HTTP/3, edge computing
- Frontend patterns sostenibili: lazy loading, image optimization, dark mode energy
- API design sostenibile: pagination, field selection, GraphQL vs REST
- Carbon monitoring con Prometheus, Grafana e SCI score per servizio
- Caso studio e-commerce: -45% carbon footprint applicando tutti i pattern
Serie Green Software - 10 Articoli
| # | Titolo | Focus | Stato |
|---|---|---|---|
| 1 | Principi del Green Software Engineering | GSF, SCI, 8 principi fondamentali | Pubblicato |
| 2 | Misurare le Emissioni con CodeCarbon | CodeCarbon, profiling energetico Python | Pubblicato |
| 3 | Carbon Aware SDK: Scheduling Intelligente | Spostamento temporale e geografico workload | Pubblicato |
| 4 | Climatiq API: Dati di Emissione in Tempo Reale | API emissioni, fattori di conversione | Pubblicato |
| 5 | GreenOps: Infrastructure as Code Sostenibile | Terraform green, spot instances, auto-scaling | Pubblicato |
| 6 | AI e Carbon Footprint: Training Responsabile | Modelli efficienti, LoRA, quantizzazione | Pubblicato |
| 7 | Scope 3 nelle Pipeline Software | Emissioni upstream/downstream, supply chain | Pubblicato |
| 8 | Scope Modeling: Simulare l'Impatto | Simulation, what-if analysis, roadmap verde | Pubblicato |
| 9 | Pattern Architetturali Sostenibili | Storage, Cache, Batch, API design | Questo articolo |
| 10 | ESG, CSRD e Compliance per Software Team | Reporting obbligatorio, audit trail, metriche ESG | Prossimo |
Storage Tiering: Il Dato Giusto nel Posto Giusto
Il primo grande spreco energetico nei sistemi moderni è lo storage non ottimizzato: dati acceduti raramente che risiedono su SSD NVMe ad alta performance, o peggio, in memoria RAM. Un SSD enterprise consuma fino a 6-10 Watt in idle e 25 Watt sotto carico. Storage su HDD consuma 5-8 Watt. Storage su tape o archivio a freddo consuma meno di 0.01 Watt per TB in idle.
La strategia di tiered storage consiste nel classificare i dati in base alla frequenza di accesso e spostarli automaticamente nel livello di storage energeticamente appropriato. Un'organizzazione media che implementa correttamente il tiered storage riduce i costi di storage del 40-70% e il relativo carbon footprint in proporzione simile.
Architettura a 4 Livelli di Storage
| Livello | Tipo | Latenza | Costo/TB/mese | Energia/TB | Uso Tipico |
|---|---|---|---|---|---|
| Hot | NVMe SSD / Redis | < 1ms | $200-500 | Alto (25W) | Dati attivi ultimi 30 giorni |
| Warm | SSD Standard / S3 Standard | 1-10ms | $20-50 | Medio (8W) | Dati 1-12 mesi, accesso settimanale |
| Cold | HDD / S3 IA / GCS Nearline | 50-250ms | $4-10 | Basso (3W) | Dati 1-7 anni, accesso mensile |
| Archive | Glacier / GCS Archive / Tape | 1-12 ore | $0.40-1 | Minimo (<0.01W) | Dati >7 anni, compliance, backup |
Implementare Politiche di Data Lifecycle Automatiche
Il cuore del tiered storage è l'automazione delle politiche di lifecycle. Invece di gestire manualmente lo spostamento dei dati, definiamo regole che il sistema applica automaticamente in base all'età e alla frequenza di accesso dei dati.
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional
import boto3
import logging
logger = logging.getLogger(__name__)
class StorageTier(Enum):
HOT = "hot"
WARM = "warm"
COLD = "cold"
ARCHIVE = "archive"
# Energia media per operazione per tier (Wh per GB spostato/acceduto)
TIER_ENERGY_PROFILE = {
StorageTier.HOT: {"idle_w_per_tb": 25, "access_wh_per_gb": 0.002},
StorageTier.WARM: {"idle_w_per_tb": 8, "access_wh_per_gb": 0.0008},
StorageTier.COLD: {"idle_w_per_tb": 3, "access_wh_per_gb": 0.0003},
StorageTier.ARCHIVE: {"idle_w_per_tb": 0.01, "access_wh_per_gb": 0.00001},
}
@dataclass(frozen=True)
class LifecyclePolicy:
"""Immutabile: definisce le soglie di transizione tra tier."""
hot_to_warm_days: int = 30
warm_to_cold_days: int = 90
cold_to_archive_days: int = 365
delete_after_days: Optional[int] = None # None = mantieni forever
@dataclass(frozen=True)
class DataAsset:
"""Rappresenta un asset dati con le sue metriche."""
key: str
size_gb: float
created_at: datetime
last_accessed: datetime
current_tier: StorageTier
access_count_30d: int
class DataLifecycleManager:
"""Gestisce il lifecycle dei dati verso tier energeticamente ottimali."""
def __init__(self, policy: LifecyclePolicy, s3_client=None):
self._policy = policy
self._s3 = s3_client or boto3.client("s3")
def evaluate_tier_transition(self, asset: DataAsset) -> Optional[StorageTier]:
"""
Determina se un asset deve essere spostato.
Ritorna il nuovo tier o None se nessun cambio necessario.
"""
age_days = (datetime.utcnow() - asset.created_at).days
days_since_access = (datetime.utcnow() - asset.last_accessed).days
# Regola 1: archivio per dati molto vecchi
if age_days > self._policy.cold_to_archive_days:
if asset.current_tier != StorageTier.ARCHIVE:
return StorageTier.ARCHIVE
# Regola 2: cold per dati accessati raramente
elif age_days > self._policy.warm_to_cold_days and asset.access_count_30d < 2:
if asset.current_tier in (StorageTier.HOT, StorageTier.WARM):
return StorageTier.COLD
# Regola 3: warm per dati maturi con accesso moderato
elif age_days > self._policy.hot_to_warm_days and asset.access_count_30d < 10:
if asset.current_tier == StorageTier.HOT:
return StorageTier.WARM
return None # Nessun cambio necessario
def calculate_carbon_savings(
self,
asset: DataAsset,
target_tier: StorageTier,
carbon_intensity_g_kwh: float = 350 # Media europea 2025
) -> dict:
"""Stima il risparmio CO2 mensile dal cambio di tier."""
current_profile = TIER_ENERGY_PROFILE[asset.current_tier]
target_profile = TIER_ENERGY_PROFILE[target_tier]
# Energia idle mensile (30 giorni * 24 ore)
current_idle_wh = (current_profile["idle_w_per_tb"] * asset.size_gb / 1000) * 720
target_idle_wh = (target_profile["idle_w_per_tb"] * asset.size_gb / 1000) * 720
savings_wh = current_idle_wh - target_idle_wh
savings_kwh = savings_wh / 1000
savings_co2_g = savings_kwh * carbon_intensity_g_kwh
return {
"monthly_savings_kwh": round(savings_kwh, 4),
"monthly_savings_co2_g": round(savings_co2_g, 2),
"annual_savings_co2_kg": round(savings_co2_g * 12 / 1000, 3),
}
def execute_transition(self, asset: DataAsset, target_tier: StorageTier, bucket: str) -> dict:
"""Sposta l'asset al nuovo tier su S3 con storage class appropriata."""
storage_class_map = {
StorageTier.WARM: "STANDARD_IA",
StorageTier.COLD: "GLACIER_IR",
StorageTier.ARCHIVE: "DEEP_ARCHIVE",
}
storage_class = storage_class_map.get(target_tier, "STANDARD")
savings = self.calculate_carbon_savings(asset, target_tier)
try:
# Copia con nuova storage class (immutabile: crea nuovo oggetto S3)
self._s3.copy_object(
CopySource={"Bucket": bucket, "Key": asset.key},
Bucket=bucket,
Key=asset.key,
StorageClass=storage_class,
MetadataDirective="COPY",
)
logger.info(
"Tier transition: %s -> %s | CO2 saved: %.2fg/month",
asset.current_tier.value, target_tier.value,
savings["monthly_savings_co2_g"]
)
return {"success": True, "savings": savings}
except Exception as e:
logger.error("Transition failed for %s: %s", asset.key, e)
return {"success": False, "error": str(e)}
# AWS S3 Lifecycle Policy come JSON (alternativa Infrastructure-as-Code)
S3_LIFECYCLE_POLICY = {
"Rules": [
{
"ID": "GreenDataLifecycle",
"Status": "Enabled",
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER_IR"},
{"Days": 365, "StorageClass": "DEEP_ARCHIVE"},
],
"Expiration": {"Days": 2555}, # 7 anni poi elimina
}
]
}
Compressione: Il Pattern Più Sottovalutato
La compressione dei dati riduce contemporaneamente il consumo di storage (meno byte da mantenere) e il traffico di rete (meno byte da trasferire). Il rapporto energia risparmiata / energia spesa per la compressione è tipicamente 10:1 a 100:1, rendendolo uno dei pattern più energeticamente vantaggiosi.
import zlib
import lz4.frame
import zstandard as zstd
import time
from dataclasses import dataclass
from typing import Callable
@dataclass(frozen=True)
class CompressionProfile:
"""Profilo immutabile di un algoritmo di compressione."""
name: str
compress_fn: Callable[[bytes], bytes]
decompress_fn: Callable[[bytes], bytes]
cpu_intensity: float # 1.0 = baseline, <1 = meno CPU, >1 = più CPU
best_for: str
def benchmark_compression(data: bytes, profile: CompressionProfile) -> dict:
"""Misura efficienza energetica di un algoritmo su dati reali."""
# Compressione
start = time.perf_counter()
compressed = profile.compress_fn(data)
compress_ms = (time.perf_counter() - start) * 1000
# Decompressione
start = time.perf_counter()
profile.decompress_fn(compressed)
decompress_ms = (time.perf_counter() - start) * 1000
ratio = len(data) / len(compressed)
# Energia stimata: CPU_time * intensità * 0.001 Wh per ms di CPU
compress_energy_mwh = compress_ms * profile.cpu_intensity * 0.001
storage_savings_pct = (1 - 1/ratio) * 100
return {
"algorithm": profile.name,
"ratio": round(ratio, 2),
"storage_savings_pct": round(storage_savings_pct, 1),
"compress_ms": round(compress_ms, 2),
"decompress_ms": round(decompress_ms, 2),
"compress_energy_mwh": round(compress_energy_mwh, 4),
"best_for": profile.best_for,
}
# Profili degli algoritmi principali
COMPRESSION_PROFILES = [
CompressionProfile(
name="zlib-6",
compress_fn=lambda d: zlib.compress(d, level=6),
decompress_fn=zlib.decompress,
cpu_intensity=1.0,
best_for="Compatibilità universale, HTTP responses"
),
CompressionProfile(
name="lz4",
compress_fn=lz4.frame.compress,
decompress_fn=lz4.frame.decompress,
cpu_intensity=0.15, # Molto veloce, meno CPU
best_for="Stream real-time, alta frequenza di accesso"
),
CompressionProfile(
name="zstd-3",
compress_fn=lambda d: zstd.ZstdCompressor(level=3).compress(d),
decompress_fn=lambda d: zstd.ZstdDecompressor().decompress(d),
cpu_intensity=0.4,
best_for="Bilanciamento ottimale ratio/velocità (raccomandato)"
),
CompressionProfile(
name="zstd-19",
compress_fn=lambda d: zstd.ZstdCompressor(level=19).compress(d),
decompress_fn=lambda d: zstd.ZstdDecompressor().decompress(d),
cpu_intensity=2.5, # Alta CPU per compressione massima
best_for="Archivio cold/archive, dati rari, batch notturno"
),
]
# Regola pratica per scelta del livello di compressione:
# HOT tier -> lz4 (latenza minima, decompress velocissimo)
# WARM tier -> zstd-3 (bilanciamento ottimale)
# COLD tier -> zstd-9 (ratio migliore, latenza tollerabile)
# ARCHIVE -> zstd-19 o brotli-11 (massimo risparmio storage)
Caching Carbon-Aware: Servire di Più Computando di Meno
Il caching è probabilmente il pattern più potente per ridurre le emissioni software: ogni cache hit elimina completamente il consumo energetico del calcolo corrispondente. Un sistema con cache hit rate del 90% esegue solo 1/10 delle computazioni rispetto a uno senza cache, con un risparmio energetico proporzionale.
Ma il "caching carbon-aware" va oltre la semplice ottimizzazione delle performance: considera l'intensità carbonica della grid per decidere cosa precaricare, per quanto tempo mantenere i dati in cache, e quando eseguire le operazioni di cache warming.
Architettura Multi-Level Cache
Livelli di Cache e Impatto Energetico
| Livello | Tecnologia | Latenza | Energia per Hit | Energia Risparmiata vs DB |
|---|---|---|---|---|
| L1: In-Process | HashMap, LRU in RAM | < 0.1ms | ~0.001 mWh | 99.9% risparmio |
| L2: Distributed | Redis, Memcached | 0.1-1ms | ~0.01 mWh | 99% risparmio |
| L3: CDN Edge | CloudFront, Fastly, CF | 1-20ms | ~0.05 mWh | 95% risparmio |
| DB Query | PostgreSQL, MySQL | 5-100ms | ~1-10 mWh | - baseline |
import Redis from "ioredis";
interface CacheEntry<T> {
readonly data: T;
readonly cachedAt: number;
readonly ttlMs: number;
readonly carbonIntensityAtCache: number; // gCO2/kWh quando cacheato
}
interface CarbonAwareCacheConfig {
readonly l1MaxEntries: number;
readonly l1DefaultTtlMs: number;
readonly l2DefaultTtlMs: number;
readonly lowCarbonThreshold: number; // gCO2/kWh
readonly highCarbonTtlMultiplier: number; // TTL più lungo quando carbon e alto
}
const DEFAULT_CONFIG: CarbonAwareCacheConfig = {
l1MaxEntries: 1000,
l1DefaultTtlMs: 60_000, // 1 minuto L1
l2DefaultTtlMs: 300_000, // 5 minuti L2
lowCarbonThreshold: 200, // <200 gCO2/kWh = energia verde
highCarbonTtlMultiplier: 3, // TTL 3x più lungo su energia sporca
};
class CarbonAwareMultiLevelCache<T> {
private readonly l1 = new Map<string, CacheEntry<T>>();
private readonly config: CarbonAwareCacheConfig;
private readonly redis: Redis;
private currentCarbonIntensity = 350; // Default, aggiornato periodicamente
// Metriche immutabili per reporting
private readonly metrics = {
l1Hits: 0,
l2Hits: 0,
misses: 0,
carbonSavedGrams: 0,
};
constructor(redisClient: Redis, config: Partial<CarbonAwareCacheConfig> = {}) {
this.redis = redisClient;
this.config = { ...DEFAULT_CONFIG, ...config };
}
async get(key: string): Promise<T | null> {
// L1: memoria locale (nessuna I/O, minima energia)
const l1Entry = this.l1.get(key);
if (l1Entry && !this.isExpired(l1Entry)) {
this.metrics.l1Hits++;
this.metrics.carbonSavedGrams += 0.005; // ~5mg CO2 risparmiati vs DB
return l1Entry.data;
}
// L2: Redis distribuito
try {
const raw = await this.redis.get(key);
if (raw) {
const entry: CacheEntry<T> = JSON.parse(raw);
if (!this.isExpired(entry)) {
// Promuovi in L1
this.setL1(key, entry.data, this.config.l1DefaultTtlMs);
this.metrics.l2Hits++;
this.metrics.carbonSavedGrams += 0.003; // ~3mg CO2 vs DB
return entry.data;
}
}
} catch (err) {
console.warn("L2 cache read failed, fallback to source:", err);
}
this.metrics.misses++;
return null;
}
async set(key: string, data: T): Promise<void> {
// TTL adattivo in base all'intensità carbonica corrente
// Quando l'energia e verde (low carbon), TTL più breve e accettabile
// Quando l'energia e "sporca" (high carbon), TTL più lungo per ridurre ricalcoli
const isHighCarbon = this.currentCarbonIntensity > this.config.lowCarbonThreshold;
const ttlMultiplier = isHighCarbon ? this.config.highCarbonTtlMultiplier : 1;
const l1TtlMs = this.config.l1DefaultTtlMs * ttlMultiplier;
const l2TtlMs = this.config.l2DefaultTtlMs * ttlMultiplier;
this.setL1(key, data, l1TtlMs);
// Scrivi in Redis in modo non bloccante
const entry: CacheEntry<T> = {
data,
cachedAt: Date.now(),
ttlMs: l2TtlMs,
carbonIntensityAtCache: this.currentCarbonIntensity,
};
await this.redis.set(key, JSON.stringify(entry), "PX", l2TtlMs);
}
private setL1(key: string, data: T, ttlMs: number): void {
// Evict LRU se pieno
if (this.l1.size >= this.config.l1MaxEntries) {
const firstKey = this.l1.keys().next().value;
if (firstKey) this.l1.delete(firstKey);
}
this.l1.set(key, {
data,
cachedAt: Date.now(),
ttlMs,
carbonIntensityAtCache: this.currentCarbonIntensity,
});
}
private isExpired(entry: CacheEntry<T>): boolean {
return Date.now() - entry.cachedAt > entry.ttlMs;
}
updateCarbonIntensity(intensityGCO2PerKWh: number): void {
this.currentCarbonIntensity = intensityGCO2PerKWh;
}
getMetrics(): Readonly<typeof this.metrics> {
const total = this.metrics.l1Hits + this.metrics.l2Hits + this.metrics.misses;
return {
...this.metrics,
hitRate: total ? ((this.metrics.l1Hits + this.metrics.l2Hits) / total * 100).toFixed(1) + "%" : "N/A",
} as any;
}
}
CDN Carbon-Aware: Servire dall'Edge Verde
Un CDN come Cloudflare o Fastly ha edge node in decine di region con intensità carbonica molto diversa. Instradare il traffico verso gli edge con energia più verde, quando la latenza lo permette, può ridurre le emissioni del serving del 20-40%.
// Cache invalidation: uno dei problemi più difficili del software
// Pattern carbon-aware: invalida in batch durante finestre di bassa carbon intensity
interface InvalidationJob {
readonly tags: readonly string[];
readonly priority: "immediate" | "carbon-optimal" | "batch-night";
readonly scheduledAt: Date;
readonly maxDelayMs: number;
}
class CarbonAwareCacheInvalidator {
private readonly pendingJobs: InvalidationJob[] = [];
private readonly carbonAwareSdk: any; // Carbon Aware SDK instance
async scheduleInvalidation(
tags: string[],
priority: InvalidationJob["priority"] = "carbon-optimal",
maxDelayMs: number = 3_600_000 // 1 ora di tolleranza
): Promise<{ jobId: string; scheduledFor: Date }> {
if (priority === "immediate") {
await this.executePurge(tags);
return { jobId: crypto.randomUUID(), scheduledFor: new Date() };
}
// Trova la finestra con minore carbon intensity nelle prossime maxDelayMs
const optimalTime = await this.findGreenWindow(maxDelayMs);
const job: InvalidationJob = {
tags: Object.freeze(tags),
priority,
scheduledAt: optimalTime,
maxDelayMs,
};
// Immutabile: non mutiamo pendingJobs esistenti, aggiungiamo nuovo
this.pendingJobs.push(job);
return {
jobId: crypto.randomUUID(),
scheduledFor: optimalTime,
};
}
private async findGreenWindow(maxDelayMs: number): Promise<Date> {
const windowEnd = new Date(Date.now() + maxDelayMs);
try {
// Carbon Aware SDK: trova il momento con minore intensità carbonica
const forecast = await this.carbonAwareSdk.getForecast({
location: "westeurope",
start: new Date(),
end: windowEnd,
duration: 15, // Job richiede ~15 minuti
});
return new Date(forecast.optimalWindow.start);
} catch {
// Fallback: esegui immediatamente se il forecast non e disponibile
return new Date();
}
}
private async executePurge(tags: string[]): Promise<void> {
// Cloudflare Cache Tag Purge API
await fetch("https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache", {
method: "POST",
headers: {
"Authorization": "Bearer CF_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({ tags }),
});
}
}
Batch Processing Carbon-Aware: Lavora Quando l'Energia È Verde
Il batch processing è il candidato ideale per la temporal shifting, il principio che consiste nello spostare i carichi di lavoro flessibili nei momenti in cui l'intensità carbonica della rete elettrica è più bassa. Un sistema che fa girare i propri batch notturni quando l'eolico e il solare producono surplus energetico può ridurre le emissioni associate del 30-70% rispetto a un'esecuzione a orario fisso.
Il Paradosso del Batch alle 02:00
Molti sistemi schedulano i batch alle 02:00 "perché c'è meno traffico". Ma in Europa la notte non è sempre il momento con minore carbon intensity: in molte regioni il sole solare e assente la notte e il vento varia. In alcune zone, le ore 10-14 (solare al picco) o le ore 02-06 (vento costante) hanno intensità carbonica molto più bassa. Usare i dati reali del Carbon Aware SDK invece di rule-of-thumb temporali.
from celery import Celery
from celery.schedules import crontab
from datetime import datetime, timedelta
from typing import Optional, NamedTuple
import httpx
import asyncio
import logging
logger = logging.getLogger(__name__)
app = Celery("green_batch", broker="redis://localhost:6379/0")
class GreenWindow(NamedTuple):
"""Finestra di esecuzione ottimale per carbon footprint."""
start: datetime
end: datetime
carbon_intensity_g_kwh: float
savings_pct_vs_now: float
class CarbonAwareBatchScheduler:
"""Schedula batch job nelle finestre a minore carbon intensity."""
BASE_CARBON_API = "https://api.electricitymap.org/v3"
def __init__(self, api_token: str, location: str = "IT"):
self._token = api_token
self._location = location
async def get_optimal_window(
self,
job_duration_minutes: int,
max_delay_hours: int = 12,
min_savings_pct: float = 15.0
) -> Optional[GreenWindow]:
"""
Trova la finestra ottimale per eseguire un batch job.
Ritorna None se nessuna finestra con risparmio sufficiente trovata.
"""
headers = {"auth-token": self._token}
async with httpx.AsyncClient() as client:
# Intensità carbonica attuale
current_resp = await client.get(
f"{self.BASE_CARBON_API}/carbon-intensity/latest",
params={"zone": self._location},
headers=headers
)
current_data = current_resp.json()
current_intensity = current_data["carbonIntensity"]
# Forecast delle prossime ore
forecast_resp = await client.get(
f"{self.BASE_CARBON_API}/carbon-intensity/forecast",
params={"zone": self._location},
headers=headers
)
forecast = forecast_resp.json()
now = datetime.utcnow()
deadline = now + timedelta(hours=max_delay_hours)
best_window: Optional[GreenWindow] = None
min_intensity = current_intensity
for slot in forecast["forecast"]:
slot_time = datetime.fromisoformat(slot["datetime"].replace("Z", "+00:00"))
slot_time = slot_time.replace(tzinfo=None)
if slot_time < now or slot_time > deadline:
continue
intensity = slot["carbonIntensity"]
if intensity < min_intensity:
min_intensity = intensity
savings_pct = (current_intensity - intensity) / current_intensity * 100
if savings_pct >= min_savings_pct:
best_window = GreenWindow(
start=slot_time,
end=slot_time + timedelta(minutes=job_duration_minutes),
carbon_intensity_g_kwh=intensity,
savings_pct_vs_now=round(savings_pct, 1),
)
return best_window
async def schedule_green(
self,
task_name: str,
job_duration_minutes: int,
task_kwargs: dict,
max_delay_hours: int = 12
) -> dict:
"""Schedula un Celery task nella finestra più verde."""
window = await self.get_optimal_window(
job_duration_minutes=job_duration_minutes,
max_delay_hours=max_delay_hours
)
if window:
delay_seconds = (window.start - datetime.utcnow()).total_seconds()
task = app.send_task(
task_name,
kwargs=task_kwargs,
countdown=max(0, int(delay_seconds))
)
logger.info(
"Scheduled '%s' for %s (%.1f%% CO2 savings, %.0fg/kWh)",
task_name, window.start.isoformat(),
window.savings_pct_vs_now, window.carbon_intensity_g_kwh
)
return {
"task_id": task.id,
"scheduled_for": window.start.isoformat(),
"carbon_intensity": window.carbon_intensity_g_kwh,
"co2_savings_pct": window.savings_pct_vs_now,
}
else:
# Nessuna finestra verde disponibile: esegui ora
task = app.send_task(task_name, kwargs=task_kwargs)
logger.warning("No green window found for '%s', executing immediately", task_name)
return {"task_id": task.id, "scheduled_for": "now", "co2_savings_pct": 0}
# Definizione dei task Celery con metriche carbon
@app.task(name="batch.nightly_report", bind=True)
def nightly_report_batch(self, report_date: str) -> dict:
"""
Genera report notturni. Non time-critical: ideale per temporal shifting.
Carbon savings tipici: 20-60% spostando dalle 02:00 alla finestra verde.
"""
logger.info("Generating report for %s (carbon-optimal execution)", report_date)
# ... logica report
return {"status": "completed", "report_date": report_date}
@app.task(name="batch.data_sync", bind=True)
def data_sync_batch(self, source: str) -> dict:
"""
Sincronizzazione dati tra sistemi. Tollerante a ritardi di alcune ore.
"""
# ... logica sync
return {"status": "synced", "source": source}
# Scheduler che usa carbon-awareness invece di crontab fisso
async def schedule_nightly_jobs():
scheduler = CarbonAwareBatchScheduler(
api_token="your_electricity_maps_token",
location="IT" # Italia
)
# Report: accetta fino a 12h di ritardo
await scheduler.schedule_green(
task_name="batch.nightly_report",
job_duration_minutes=45,
task_kwargs={"report_date": datetime.now().strftime("%Y-%m-%d")},
max_delay_hours=12
)
# Data sync: accetta fino a 6h di ritardo
await scheduler.schedule_green(
task_name="batch.data_sync",
job_duration_minutes=20,
task_kwargs={"source": "salesforce"},
max_delay_hours=6
)
Right-Sizing e Auto-Scaling Down: Fermare lo Spreco Invisibile
Uno studio di Gartner del 2025 stima che il 35-40% delle risorse cloud enterprise sono over-provisioned: server che girano al 10-15% di CPU, database con 90% di memoria inutilizzata, Lambda functions con 3GB di RAM allocata quando ne bastano 256MB. Questo "idle computing" è puro spreco energetico.
Il right-sizing sistematico - ridurre le risorse al minimo necessario per soddisfare i requisiti di performance - è spesso il singolo intervento con il miglior ROI energetico. Non richiede riscrittura del codice: è una questione di configurazione e monitoraggio.
Strategie di Right-Sizing per Carbon Reduction
| Strategia | Risparmio Tipico | Complessità | Rischio |
|---|---|---|---|
| Ridurre size istanze over-provisioned | 20-40% | Bassa | Basso (rollback facile) |
| Auto-scaling down aggressivo | 30-60% | Media | Medio (latenza cold start) |
| Serverless per workload intermittenti | 50-90% | Alta | Medio (cold start, vendor lock-in) |
| Spot/Preemptible instances per batch | 60-80% | Alta | Alto (interruzioni) |
| Schedule shutdown fuori orario (dev/staging) | 40-70% | Bassa | Nullo (ambienti non-prod) |
import boto3
from datetime import datetime
from dataclasses import dataclass
from typing import Sequence
@dataclass(frozen=True)
class ScalingPolicy:
"""Politica di scaling immutabile con carbon awareness."""
min_capacity: int
max_capacity: int
target_cpu_pct: float
scale_in_cooldown_sec: int
green_hour_min_capacity: int # capacità minima durante ore verdi
low_traffic_scale_in_factor: float # Fattore aggressivo in ore basse traffico
def create_carbon_aware_scaling_policies(
asg_name: str,
policy: ScalingPolicy,
region: str = "eu-west-1"
) -> dict:
"""
Configura Auto Scaling Group con politiche carbon-aware.
Scale-in più aggressivo durante ore di basso traffico (di notte)
dove l'energia potrebbe essere più verde E il traffico e basso.
"""
autoscaling = boto3.client("autoscaling", region_name=region)
# Policy principale: target tracking su CPU
main_policy = autoscaling.put_scaling_policy(
AutoScalingGroupName=asg_name,
PolicyName=f"{asg_name}-carbon-aware-cpu",
PolicyType="TargetTrackingScaling",
TargetTrackingConfiguration={
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization"
},
"TargetValue": policy.target_cpu_pct,
"ScaleInCooldown": policy.scale_in_cooldown_sec,
"ScaleOutCooldown": 120,
"DisableScaleIn": False,
}
)
# Scheduled action: scale down notturno aggressivo
# Combina basso traffico + potenziale energia verde
autoscaling.put_scheduled_update_group_action(
AutoScalingGroupName=asg_name,
ScheduledActionName=f"{asg_name}-night-scaledown",
Recurrence="0 22 * * *", # Ogni giorno alle 22:00 UTC
MinSize=policy.green_hour_min_capacity,
MaxSize=policy.max_capacity,
DesiredCapacity=policy.green_hour_min_capacity,
)
# Scheduled action: scale up mattutino prima del traffico
autoscaling.put_scheduled_update_group_action(
AutoScalingGroupName=asg_name,
ScheduledActionName=f"{asg_name}-morning-scaleup",
Recurrence="0 7 * * MON-FRI", # Lun-Ven alle 07:00 UTC
MinSize=policy.min_capacity,
MaxSize=policy.max_capacity,
DesiredCapacity=policy.min_capacity + 2, # Pre-warm prima del traffico
)
return {
"asg_name": asg_name,
"main_policy_arn": main_policy["PolicyARN"],
"estimated_monthly_co2_reduction_pct": 35, # Tipico per questo pattern
}
# Lambda Right-sizing: trova la memoria ottimale
# Principio: RAM in eccesso = costo carbonio inutile
def optimize_lambda_memory(function_name: str, region: str = "eu-west-1") -> dict:
"""
Analizza l'utilizzo memoria di una Lambda e suggerisce il right-size.
AWS Lambda Power Tuning (tool open source) automatizza questo processo.
"""
lambda_client = boto3.client("lambda", region_name=region)
cloudwatch = boto3.client("cloudwatch", region_name=region)
# Recupera configurazione attuale
config = lambda_client.get_function_configuration(FunctionName=function_name)
current_memory_mb = config["MemorySize"]
# Recupera metriche CloudWatch: max memory used negli ultimi 7 giorni
# (in produzione usare AWS Lambda Power Tuning per analisi completa)
metrics = cloudwatch.get_metric_statistics(
Namespace="AWS/Lambda",
MetricName="MaxMemoryUsed",
Dimensions=[{"Name": "FunctionName", "Value": function_name}],
StartTime=datetime.utcnow().replace(hour=0, minute=0) - __import__("datetime").timedelta(days=7),
EndTime=datetime.utcnow(),
Period=86400,
Statistics=["Maximum"],
)
if not metrics["Datapoints"]:
return {"status": "insufficient_data"}
max_used_mb = max(dp["Maximum"] for dp in metrics["Datapoints"])
# Buffer di sicurezza: 30% sopra il massimo osservato
recommended_mb = min(int(max_used_mb * 1.3 / 64 + 1) * 64, 10240)
co2_reduction_pct = max(0, (current_memory_mb - recommended_mb) / current_memory_mb * 100)
return {
"current_memory_mb": current_memory_mb,
"max_observed_mb": int(max_used_mb),
"recommended_mb": recommended_mb,
"potential_co2_reduction_pct": round(co2_reduction_pct, 1),
"annual_cost_savings_usd": (current_memory_mb - recommended_mb) / 1024 * 0.0000166667 * 3_600_000 * 12,
}
Database Pattern Sostenibili: Meno Query, Meno Carbon
Il database è spesso il componente con il maggior consumo energetico di un sistema enterprise. Ogni query comporta I/O su disco, allocazione di buffer in RAM, cicli CPU per parsing, planning e execution. Ottimizzare le query non è solo una questione di performance: è una riduzione diretta delle emissioni.
Pattern 1: Materialized Views per Ridurre Ricalcoli
Le materialized views sono il pattern più efficace per eliminare query aggregate costose che vengono rieseguite continuamente. Invece di ricalcolare SUM, COUNT, JOIN complessi ad ogni richiesta, si precalcola il risultato e lo si aggiorna periodicamente o tramite trigger.
-- PROBLEMA: Query aggregata pesante eseguita 1000x al giorno
-- Ogni esecuzione: 2-5 secondi, 100-500ms CPU, I/O intensivo
-- Stima: ~500mWh/giorno solo per questa query
-- Query pesante PRIMA (eseguita ad ogni richiesta)
SELECT
c.category_id,
c.name AS category_name,
COUNT(DISTINCT o.order_id) AS total_orders,
SUM(oi.quantity * oi.unit_price) AS total_revenue,
AVG(oi.quantity * oi.unit_price) AS avg_order_value,
COUNT(DISTINCT o.customer_id) AS unique_customers
FROM categories c
JOIN products p ON p.category_id = c.category_id
JOIN order_items oi ON oi.product_id = p.product_id
JOIN orders o ON o.order_id = oi.order_id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY c.category_id, c.name;
-- SOLUZIONE: Materialized view con refresh in finestra verde
CREATE MATERIALIZED VIEW mv_category_metrics_30d AS
SELECT
c.category_id,
c.name AS category_name,
COUNT(DISTINCT o.order_id) AS total_orders,
SUM(oi.quantity * oi.unit_price) AS total_revenue,
AVG(oi.quantity * oi.unit_price) AS avg_order_value,
COUNT(DISTINCT o.customer_id) AS unique_customers,
NOW() AS last_refreshed
FROM categories c
JOIN products p ON p.category_id = c.category_id
JOIN order_items oi ON oi.product_id = p.product_id
JOIN orders o ON o.order_id = oi.order_id
WHERE o.created_at >= NOW() - INTERVAL '30 days'
GROUP BY c.category_id, c.name
WITH DATA;
-- Indice per query O(1)
CREATE UNIQUE INDEX idx_mv_category_metrics ON mv_category_metrics_30d (category_id);
-- Refresh schedulato: CONCURRENT permette letture durante il refresh
-- Schedulare nelle finestre verdi (es. con pg_cron + carbon intensity check)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_metrics_30d;
-- Con pg_cron (schedulare refresh ogni ora nelle ore diurne con solare)
SELECT cron.schedule(
'refresh-category-metrics',
'0 * * * *', -- Ogni ora; logica carbon-aware nell'applicazione
'REFRESH MATERIALIZED VIEW CONCURRENTLY mv_category_metrics_30d'
);
-- Query dopo: O(1) sul materialized view
-- Stima risparmio: 99% della computazione originale
SELECT * FROM mv_category_metrics_30d ORDER BY total_revenue DESC;
-- Pattern 2: Read Replica per separare carichi
-- Letture analitiche (intensive) -> read replica
-- Scritture -> primary (minimo carico)
-- Pattern 3: Partial Indexes per ridurre I/O
-- INVECE DI: indice su tutti i 50M ordini
CREATE INDEX idx_orders_status_all ON orders(status, created_at);
-- MEGLIO: indice solo sui 500K ordini attivi (1% del totale)
-- 99% meno I/O, 99% meno spazio, maintenance molto più veloce
CREATE INDEX idx_orders_status_active ON orders(status, created_at)
WHERE status IN ('pending', 'processing', 'shipped');
-- Pattern 4: Connection Pooling per ridurre overhead
-- PgBouncer: max_client_conn=1000, pool_size=20
-- Riduce: TCP handshakes, SSL negotiation, process fork overhead
-- Stima risparmio: 30-50% CPU PostgreSQL su workload high-concurrency
Query Optimization: N+1 Problem e Eager Loading
Il problema N+1 è uno dei più comuni e costosi anti-pattern in termini di emissioni: invece di una singola query che recupera tutti i dati necessari, si eseguono N+1 query separate. Con N=1000 ordini, si generano 1001 query invece di 1, moltiplicando per 1000 il carbon footprint dell'operazione.
from sqlalchemy import select
from sqlalchemy.orm import selectinload, joinedload, Session
from typing import Sequence
# ANTI-PATTERN: N+1 queries - EVITARE
def get_orders_naive(session: Session, limit: int = 100) -> list:
"""
PROBLEMATICO: per 100 ordini genera 101 query.
100 ordini -> 1 query
100 customer -> 100 query separate
Stima: ~2mWh per 100 ordini. Su 10.000 richieste/giorno = 20Wh/giorno.
"""
orders = session.execute(select(Order).limit(limit)).scalars().all()
# Ogni accesso a order.customer triggersa una nuova query! (lazy loading)
return [{"id": o.id, "customer": o.customer.email} for o in orders]
# PATTERN GREEN: Eager loading con selectin
def get_orders_green(session: Session, limit: int = 100) -> list:
"""
OTTIMIZZATO: 2 query totali invece di N+1.
Query 1: tutti gli ordini
Query 2: tutti i customer in una sola query IN
Stima: 0.02mWh per 100 ordini. Risparmio: 99%.
"""
stmt = (
select(Order)
.options(selectinload(Order.customer)) # 2 query totali
.limit(limit)
)
orders = session.execute(stmt).scalars().all()
return [{"id": o.id, "customer": o.customer.email} for o in orders]
# Ancora meglio: joinedload per 1 sola query
def get_orders_single_query(session: Session, limit: int = 100) -> list:
"""
ULTRA-OTTIMIZZATO: 1 sola query con JOIN.
Ideale quando il numero di relazioni e basso.
Stima: 0.01mWh per 100 ordini. Risparmio: 99.5%.
"""
stmt = (
select(Order)
.options(joinedload(Order.customer)) # JOIN: 1 sola query
.limit(limit)
)
orders = session.execute(stmt).unique().scalars().all()
return [{"id": o.id, "customer": o.customer.email} for o in orders]
# Pattern: Projection - seleziona solo i campi necessari
def get_order_summary(session: Session, order_ids: list[int]) -> list[dict]:
"""
Seleziona SOLO i campi necessari, non SELECT *.
Su tabelle con 50+ colonne, SELECT * trasferisce 10-20x più dati.
"""
stmt = (
select(
Order.id,
Order.total_amount,
Order.status,
# Solo 3 campi invece di 50+
)
.where(Order.id.in_(order_ids))
)
rows = session.execute(stmt).all()
return [{"id": r.id, "total": float(r.total_amount), "status": r.status} for r in rows]
Network Efficiency: Ogni Byte Trasferito Ha un Costo Carbon
Trasferire dati ha un costo energetico reale: 0.06-0.1 kWh per GB per il traffico internet (backbone + last mile). Un'applicazione che trasferisce 10TB di dati non compressi al giorno consuma circa 600-1000 kWh solo per la trasmissione, equivalente a 200-400 kg CO₂ al giorno (con intensità media europea di 350 gCO₂/kWh).
HTTP/3 e QUIC: Efficienza a Livello di Protocollo
HTTP/3 con QUIC elimina il problema del head-of-line blocking e riduce i round-trip necessari per stabilire connessioni. Per applicazioni con molte richieste concorrenti, HTTP/3 può ridurre la latenza del 15-30% e, di conseguenza, il tempo di CPU attivo dei server.
import express, { Request, Response, NextFunction } from "express";
import compression from "compression";
import { createBrotliCompress, createGzip } from "zlib";
import { pipeline } from "stream/promises";
const app = express();
// Compressione intelligente: scegli il miglior algoritmo
// Brotli: migliore ratio (15-25% superiore a gzip), supportato da tutti i browser moderni
// Gzip: fallback per client vecchi
app.use(compression({
// Comprimi solo se il risparmio e significativo
threshold: 1024, // Min 1KB per comprimere
level: 6, // Bilanciamento CPU/ratio
filter: (req, res) => {
// Non comprimere immagini già compresse (jpeg, webp, png)
const contentType = res.getHeader("Content-Type") as string || "";
if (contentType.includes("image/")) return false;
return compression.filter(req, res);
},
}));
// Middleware: risposta minima con field selection
// Invece di returnare tutto l'oggetto, risponde solo con i campi richiesti
function fieldSelectionMiddleware(req: Request, res: Response, next: NextFunction): void {
const originalJson = res.json.bind(res);
res.json = (body: any) => {
const fields = req.query["fields"];
if (!fields || typeof fields !== "string" || !body) {
return originalJson(body);
}
const requestedFields = fields.split(",").map(f => f.trim());
// Filtra solo i campi richiesti (non muta body originale)
const filtered = Array.isArray(body)
? body.map(item => pickFields(item, requestedFields))
: pickFields(body, requestedFields);
return originalJson(filtered);
};
next();
}
function pickFields(obj: Record<string, unknown>, fields: string[]): Record<string, unknown> {
return fields.reduce<Record<string, unknown>>((acc, field) => {
if (field in obj) {
return { ...acc, [field]: obj[field] };
}
return acc;
}, {});
}
app.use(fieldSelectionMiddleware);
// Esempio: GET /api/users?fields=id,name,email
// Invece di returnare 50 campi, restituisce solo 3
// Riduzione payload tipica: 60-90%
// Cache-Control headers: riduce richieste ripetute
function addCacheHeaders(res: Response, maxAgeSeconds: number): void {
res.setHeader("Cache-Control", `public, max-age={maxAgeSeconds}, stale-while-revalidate=60`);
res.setHeader("Vary", "Accept-Encoding, Accept");
}
app.get("/api/products/:id", async (req: Request, res: Response) => {
const product = await getProduct(req.params["id"]);
// Dati statici: cache aggressiva
if (product?.isStatic) {
addCacheHeaders(res, 86400); // 1 giorno
} else {
addCacheHeaders(res, 300); // 5 minuti
}
res.json(product);
});
// ETag per validazione cache efficiente
// Invece di re-scaricare, il client verifica se il dato e cambiato
app.get("/api/catalog", async (req: Request, res: Response) => {
const catalog = await getCatalog();
const etag = require("crypto")
.createHash("md5")
.update(JSON.stringify(catalog))
.digest("hex");
// Se ETag non cambiato: risponde 304 (0 bytes di payload!)
if (req.headers["if-none-match"] === etag) {
res.status(304).end();
return;
}
res.setHeader("ETag", etag);
res.json(catalog);
});
async function getProduct(id: string): Promise<any> {
return { id, isStatic: true, name: "Product" };
}
async function getCatalog(): Promise<any[]> {
return [];
}
Frontend Pattern Sostenibili: Il Client È Parte del Problema
Il frontend è spesso il componente più trascurato nell'analisi delle emissioni software, ma eseguire JavaScript pesante su milioni di dispositivi ha un impatto aggregato enorme. Uno script JS da 500KB eseguito su 1 milione di dispositivi richiede circa 50 GWh di energia annuale sui device degli utenti - non nei nostri server.
Image Optimization: Il Guadagno Più Veloce
Le immagini rappresentano in media il 50-70% del peso di una pagina web. Passare da JPEG a WebP/AVIF riduce le dimensioni del 25-50%, con equivalente riduzione nel trasferimento dati e nel tempo di decodifica sul device dell'utente.
// angular.json - abilita image optimization built-in di Angular
// Angular 17+ ha NgOptimizedImage integrato
import { NgOptimizedImage } from '@angular/common';
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [NgOptimizedImage],
changeDetection: ChangeDetectionStrategy.OnPush, // Riduce change detection cycles
template: `
<!-- NgOptimizedImage: lazy loading + srcset automatico + WebP -->
<img
ngSrc="products/laptop-pro.jpg"
[width]="400"
[height]="300"
[priority]="isAboveFold"
loading="lazy"
decoding="async"
alt="Laptop Pro - Vista frontale"
/>
`
})
export class ProductCardComponent {
isAboveFold = false;
}
// build pipeline: conversione automatica a WebP/AVIF con sharp
// scripts/optimize-images.mjs
import sharp from 'sharp';
import { readdir, stat } from 'fs/promises';
import path from 'path';
async function optimizeImages(inputDir: string, outputDir: string): Promise<void> {
const files = await readdir(inputDir);
const imageFiles = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f));
const results = await Promise.all(imageFiles.map(async (file) => {
const inputPath = path.join(inputDir, file);
const baseName = path.basename(file, path.extname(file));
// Genera WebP (supporto universale 2025)
const webpPath = path.join(outputDir, `{baseName}.webp`);
await sharp(inputPath)
.webp({ quality: 80, effort: 6 })
.toFile(webpPath);
// Genera AVIF (compressione superiore, browser moderni)
const avifPath = path.join(outputDir, `{baseName}.avif`);
await sharp(inputPath)
.avif({ quality: 65, effort: 7 })
.toFile(avifPath);
const [origSize, webpSize, avifSize] = await Promise.all([
stat(inputPath).then(s => s.size),
stat(webpPath).then(s => s.size),
stat(avifPath).then(s => s.size),
]);
return {
file,
origKB: (origSize / 1024).toFixed(1),
webpKB: (webpSize / 1024).toFixed(1),
avifKB: (avifSize / 1024).toFixed(1),
webpSaving: ((1 - webpSize/origSize) * 100).toFixed(1) + '%',
avifSaving: ((1 - avifSize/origSize) * 100).toFixed(1) + '%',
};
}));
console.table(results);
}
Dark Mode e Risparmio Energetico su OLED
Dark Mode: Impatto Reale sui Display OLED
Sui display OLED (presenti in tutti gli smartphone premium e molti laptop dal 2024), i pixel neri consumano letteralmente 0 energia (il pixel OLED non emette luce se spento). Un'interfaccia con sfondo nero puro (#000000) su OLED può consumare fino al 60-80% meno energia del display rispetto a sfondo bianco (#FFFFFF). Con miliardi di dispositivi OLED, offrire un dark mode ben implementato è una riduzione significativa delle emissioni lato utente.
/* Dark mode: usa true black (#000000) per massimizzare risparmio OLED */
/* Anche #0d0d0d e sufficiente e più gradevole esteticamente */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a1a;
--surface: #ffffff;
}
/* Auto dark mode: si attiva in base alla preferenza di sistema */
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #000000; /* True black per OLED */
--bg-secondary: #0d0d0d; /* Quasi-nero, più comfort visivo */
--text-primary: #e8e8e8;
--surface: #111111;
}
}
/* Classe manuale per toggle */
.dark-theme {
--bg-primary: #000000;
--bg-secondary: #0d0d0d;
--text-primary: #e8e8e8;
--surface: #111111;
}
/* Ridurre motion: riduce animazioni = meno GPU = meno energia */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Font system: evita Google Fonts quando possibile */
/* Font di sistema = 0 download, rendering istantaneo */
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
/* Se font custom e necessario: usa font-display: swap per evitare blocking */
@font-face {
font-family: "CustomFont";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap; /* Non blocca rendering mentre scarica */
font-weight: 400 700; /* Variable font: 1 file per tutti i pesi */
}
API Design Sostenibile: Meno Round-Trip, Meno Carbon
La progettazione delle API ha un impatto diretto sulle emissioni: API mal progettate richiedono più round-trip, trasferiscono più dati del necessario, e costringono i client a fare più richieste per ottenere le informazioni di cui hanno bisogno.
Pagination Efficiente: Cursor vs Offset
La pagination con OFFSET è energeticamente costosa: per recuperare la pagina 100 con 20 elementi, il database deve scorrere e scartare 2000 righe (OFFSET 2000). Con cursor-based pagination, il database salta direttamente al punto corretto usando un indice, consumando energia proporzionale solo al numero di righe restituite, non a quelle saltate.
import { Pool } from "pg";
interface PaginationResult<T> {
readonly data: readonly T[];
readonly nextCursor: string | null;
readonly totalCount?: number;
}
// ANTI-PATTERN: Offset pagination
// Costo query: O(offset + limit) - cresce con la profondità di paginazione
async function getProductsOffset(
db: Pool,
page: number,
pageSize: number = 20
): Promise<PaginationResult<any>> {
const offset = page * pageSize;
// COSTOSO: il DB scorre 'offset' righe per poi scartarle
const result = await db.query(
"SELECT * FROM products ORDER BY id LIMIT $1 OFFSET $2",
[pageSize, offset]
);
// Esempio pagina 1000 con 20 items: DB scorre 20.020 righe
// Stima: 50x più lenta e 50x più dispendiosa di cursor pagination
return { data: result.rows, nextCursor: null };
}
// PATTERN GREEN: Cursor-based pagination
// Costo query: O(limit) - costante, usa indice
async function getProductsCursor(
db: Pool,
cursor: string | null,
pageSize: number = 20
): Promise<PaginationResult<any>> {
let query: string;
let params: any[];
if (cursor) {
// Decode cursor: contiene l'ID dell'ultimo elemento visto
const lastId = parseInt(Buffer.from(cursor, "base64").toString("utf-8"));
query = `
SELECT id, name, price, category_id
FROM products
WHERE id > $1
ORDER BY id ASC
LIMIT $2
`;
params = [lastId, pageSize + 1]; // +1 per sapere se c'è una pagina successiva
} else {
query = `
SELECT id, name, price, category_id
FROM products
ORDER BY id ASC
LIMIT $1
`;
params = [pageSize + 1];
}
const result = await db.query(query, params);
const rows = result.rows;
const hasMore = rows.length > pageSize;
const data = hasMore ? rows.slice(0, pageSize) : rows;
const lastItem = data[data.length - 1];
// Encode next cursor
const nextCursor = hasMore && lastItem
? Buffer.from(String(lastItem.id)).toString("base64")
: null;
return {
data,
nextCursor,
// Non calcola totalCount (costoso): usa solo se necessario con estimate
};
}
// GraphQL vs REST: quando ognuno e più efficiente
// REST: efficiente per risorse semplici e ben definite, cache HTTP nativa
// GraphQL: efficiente per UI complesse con molti componenti che richiedono dati diversi
// Pattern GraphQL carbon-aware: DataLoader per batch N+1
import DataLoader from "dataloader";
import { GraphQLResolveInfo } from "graphql";
// Senza DataLoader: 1000 resolver -> 1000 query SQL separate
// Con DataLoader: 1000 resolver -> 1 query SQL con WHERE IN
const productLoader = new DataLoader<number, any>(
async (productIds: readonly number[]) => {
const result = await db.query(
"SELECT * FROM products WHERE id = ANY($1)",
[[...productIds]]
);
// Mappa per mantenere l'ordine corretto
const productMap = new Map(result.rows.map(p => [p.id, p]));
return productIds.map(id => productMap.get(id) || null);
},
{ batch: true, maxBatchSize: 100 } // Max 100 per query per sicurezza
);
declare const db: Pool;
Carbon Monitoring: Misurare per Migliorare
"Ciò che non si misura non si può migliorare." Il carbon monitoring a livello di servizio permette di identificare i componenti più emissivi, tracciare i progressi nel tempo, e calcolare il SCI score (Software Carbon Intensity) come KPI aziendale.
from prometheus_client import Counter, Histogram, Gauge, start_http_server
from functools import wraps
import time
import httpx
import logging
from typing import Callable, Any
logger = logging.getLogger(__name__)
# Metriche Prometheus custom per carbon monitoring
CARBON_INTENSITY_GAUGE = Gauge(
"carbon_intensity_g_co2_per_kwh",
"Current grid carbon intensity in gCO2/kWh",
["region"]
)
ENERGY_CONSUMED_COUNTER = Counter(
"energy_consumed_wh_total",
"Total energy consumed in Wh",
["service", "endpoint", "method"]
)
CARBON_EMITTED_COUNTER = Counter(
"carbon_emitted_gco2_total",
"Total carbon emitted in gCO2",
["service", "endpoint", "method"]
)
REQUEST_DURATION_HISTOGRAM = Histogram(
"http_request_duration_seconds",
"HTTP request duration",
["service", "endpoint", "method"],
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5]
)
SCI_SCORE_GAUGE = Gauge(
"sci_score_mgco2_per_request",
"Software Carbon Intensity score in mgCO2 per functional unit",
["service"]
)
# Stima consumo energetico per tipo di risorsa
# Basato su benchmark empirici per server tipici (TDP ~200W, utilization 20%)
ENERGY_ESTIMATES_WH = {
"cpu_second": 0.011, # ~11 mWh per secondo di CPU @ 200W TDP, 20% util
"memory_gb_second": 0.000375, # ~0.375 mWh per GB*s RAM
"ssd_read_gb": 0.0002, # ~0.2 mWh per GB letto da SSD
"ssd_write_gb": 0.0004, # ~0.4 mWh per GB scritto su SSD
"network_gb": 0.1, # ~100 mWh per GB trasferito (rete)
}
class CarbonMetricsCollector:
"""Raccoglie metriche carbon per Prometheus + Grafana."""
def __init__(self, service_name: str, region: str = "IT"):
self._service = service_name
self._region = region
self._current_intensity = 350.0 # Default gCO2/kWh
async def update_carbon_intensity(self) -> None:
"""Aggiorna l'intensità carbonica dalla grid in tempo reale."""
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(
"https://api.electricitymap.org/v3/carbon-intensity/latest",
params={"zone": self._region},
headers={"auth-token": "YOUR_TOKEN"}
)
data = resp.json()
self._current_intensity = data["carbonIntensity"]
CARBON_INTENSITY_GAUGE.labels(region=self._region).set(self._current_intensity)
except Exception as e:
logger.warning("Carbon intensity fetch failed: %s", e)
def track_request(self, endpoint: str, method: str):
"""Decorator per tracciare carbon footprint di ogni endpoint."""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = await func(*args, **kwargs)
duration_s = time.perf_counter() - start
# Stima energia: basata sulla durata (proxy per CPU+I/O)
estimated_energy_wh = duration_s * ENERGY_ESTIMATES_WH["cpu_second"]
# Carbon emessa
carbon_g = estimated_energy_wh * self._current_intensity / 1000
# Aggiorna metriche Prometheus
labels = {"service": self._service, "endpoint": endpoint, "method": method}
ENERGY_CONSUMED_COUNTER.labels(**labels).inc(estimated_energy_wh * 1000) # in mWh
CARBON_EMITTED_COUNTER.labels(**labels).inc(carbon_g * 1000) # in mgCO2
REQUEST_DURATION_HISTOGRAM.labels(**labels).observe(duration_s)
return result
return wrapper
return decorator
def update_sci_score(self, total_carbon_mgco2: float, functional_units: int) -> None:
"""
Aggiorna il SCI score: mgCO2 per functional unit (es. per request).
Formula SCI: (E * I + M) / R
E = energia, I = intensità, M = embodied carbon, R = functional units
"""
if functional_units > 0:
sci = total_carbon_mgco2 / functional_units
SCI_SCORE_GAUGE.labels(service=self._service).set(sci)
# Grafana dashboard query examples (PromQL):
#
# Carbon footprint per endpoint (top 10 più emissivi):
# topk(10, sum by (endpoint) (rate(carbon_emitted_gco2_total[5m])))
#
# SCI score nel tempo:
# sci_score_mgco2_per_request{service="api-gateway"}
#
# Risparmio carbon da cache hits:
# rate(cache_hits_total[5m]) * 0.005 # 5mg CO2 per hit risparmiato
#
# Carbon intensity corrente vs soglia verde:
# carbon_intensity_g_co2_per_kwh > 300 # Alert se supera soglia
Caso Studio: E-Commerce da 1M Visite/Giorno - -45% Carbon Footprint
Vediamo come tutti questi pattern si integrano in un caso concreto. Un e-commerce con 1 milione di visite giornaliere, 50.000 ordini/giorno, e un'architettura legacy non ottimizzata. Il team ha implementato i pattern descritti in questo articolo in un progetto di 6 mesi. I risultati parlano da soli.
Baseline dell'Architettura Prima dell'Ottimizzazione
| Componente | Configurazione Prima | Problema | Carbon/mese (stima) |
|---|---|---|---|
| Web servers | 20x c5.2xlarge always-on | CPU media 8%, 92% idle | 450 kg CO₂ |
| Database | db.r5.4xlarge, SELECT * ovunque | N+1 queries, nessun indice | 280 kg CO₂ |
| Storage S3 | Tutto in Standard tier | 5 anni di log mai acceduti in hot storage | 90 kg CO₂ |
| Batch jobs | Fisso alle 02:00 UTC | Nessuna carbon awareness | 120 kg CO₂ |
| CDN/Cache | Cache hit rate 35% | TTL troppo corti, nessun edge caching | 180 kg CO₂ |
| Frontend | Bundle JS 2.8MB, JPEG | Nessuna code splitting, immagini non ottimizzate | 200 kg CO₂ |
| Totale | 1.320 kg CO₂/mese |
Risultati Dopo Ottimizzazione (6 Mesi)
| Intervento | Pattern Applicato | Riduzione Carbon | Tempo Implementazione |
|---|---|---|---|
| Auto-scaling + right-sizing EC2 | Scale down aggressivo, spot instances batch | -195 kg CO₂/mese (43%) | 2 settimane |
| Query optimization + materialized views | N+1 fix, projection, partial indexes | -140 kg CO₂/mese (50%) | 4 settimane |
| S3 lifecycle policies | Tiered storage: hot/warm/cold/archive | -72 kg CO₂/mese (80%) | 1 settimana |
| Carbon-aware batch scheduling | Carbon Aware SDK, finestre verdi | -48 kg CO₂/mese (40%) | 3 settimane |
| Multi-level cache (Redis + CDN) | Cache hit rate: 35% -> 87% | -126 kg CO₂/mese (70%) | 6 settimane |
| Frontend: WebP/AVIF + code splitting | Bundle 2.8MB -> 380KB, WebP images | -110 kg CO₂/mese (55%) | 3 settimane |
| Totale risparmiato | -691 kg CO₂/mese (-52%) | 6 mesi totali |
Il risultato finale è una riduzione del 52% del carbon footprint mensile (da 1.320 kg a 629 kg CO₂), con un risparmio annuale di circa 8.3 tonnellate di CO₂ equivalente. Equivalente a eliminare circa 4 auto dalle strade per un anno. E non è stato necessario riscrivere l'applicazione da zero: tutti gli interventi sono stati incrementali.
Lezione Chiave dal Caso Studio
I due interventi con il miglior ROI (carbon saved / effort) sono stati:
- S3 lifecycle policies (80% riduzione, 1 settimana di lavoro): bastano poche righe di configurazione Infrastructure-as-Code. È il pattern con il rapporto impatto/sforzo più alto in assoluto.
- Auto-scaling e right-sizing (43% riduzione, 2 settimane): la stragrande maggioranza dei sistemi è over-provisioned. Ridurre il provisioning al minimo necessario con auto-scaling ha un impatto immediato e misurabile.
Morale: inizia sempre dai pattern più semplici. L'80% dei guadagni viene dal 20% degli interventi.
Anti-Pattern da Evitare: Le 5 Trappole Più Comuni
Anti-Pattern #1: Cache Warming Aggressivo Fuori Orario Verde
Pre-caricare la cache in modo massiccio durante picchi di carbon intensity (tipicamente sera, quando il solare è assente e il gas copre la domanda) consuma più energia di quanta ne risparmia. Il cache warming va schedulato nelle finestre verdi identificate dal Carbon Aware SDK.
Anti-Pattern #2: Log Verbosi in Hot Storage
Conservare anni di log di debug in S3 Standard (hot tier) è uno spreco economico ed energetico. I log di più di 30 giorni raramente vengono acceduti; dopo 90 giorni, quasi mai. Implementare lifecycle policies è la prima azione da fare in qualsiasi sistema legacy.
Anti-Pattern #3: Always-On per Workload Intermittenti
Un servizio che riceve 10 richieste al giorno non ha bisogno di girare 24/7 su un'istanza dedicata. Lambda, Cloud Run o Fargate con scale-to-zero è la scelta green per workload intermittenti. Un'istanza EC2 t3.small always-on emette ~15 kg CO₂/mese; Lambda per 10 richieste/giorno emette <0.001 kg CO₂/mese. 15.000 volte meno.
Anti-Pattern #4: SELECT * in Produzione
SELECT * trasferisce tutti i campi di una tabella anche quando l'applicazione ne usa
solo 2-3. Su tabelle con 50+ colonne e milioni di righe, questo moltiplica per 10-20 i dati
trasferiti dal database, con equivalente impatto energetico. Usare sempre la projection esplicita.
Anti-Pattern #5: Bundle JavaScript Monolitico
Un bundle JS da 3MB scaricato e parsato da 1 milione di utenti al giorno richiede circa 2.000 kWh di energia CPU sui device degli utenti. Con code splitting e lazy loading, il bundle iniziale può scendere a 100-200KB, riducendo di 15-30x il consumo energetico lato client. L'energia consumata sui dispositivi degli utenti è parte dello Scope 3 del software.
Checklist: Pattern Sostenibili per il Tuo Prossimo Sprint
Priorità Alta (Impatto Massimo, Bassa Complessità)
- Implementa S3/GCS lifecycle policies per spostare dati verso tier freddi automaticamente
- Analizza e riduci il provisioning delle istanze EC2/GCE (AWS Compute Optimizer, GCP Recommender)
- Aggiungi schedule shutdown per ambienti development/staging fuori orario lavorativo
- Abilita compressione gzip/brotli su tutti gli endpoint HTTP (se non già attiva)
- Converti le immagini in WebP/AVIF nel pipeline di build
- Rimuovi tutti i
SELECT *sostituendoli con projection esplicita
Priorità Media (Alto Impatto, Complessità Moderata)
- Implementa multi-level caching (L1 in-process + L2 Redis) per le risorse più accedute
- Crea materialized views per le query aggregate eseguite frequentemente
- Migra i batch job time-flexible a Carbon Aware SDK scheduling
- Risolvi i problemi N+1 con eager loading o DataLoader (GraphQL)
- Implementa cursor-based pagination in sostituzione di offset pagination
- Aggiungi ETag e Cache-Control appropriati alle risposte API
Priorità Bassa (Miglioramenti Incrementali)
- Valuta migrazione a HTTP/3 per applicazioni ad alta latenza sensibilità
- Implementa field selection nei REST endpoint (query param
?fields=) - Aggiungi dark mode con true black per ridurre consumo su dispositivi OLED degli utenti
- Configura carbon monitoring con Prometheus + dashboard Grafana per SCI score
- Introduci partial indexes per query filtrate su subset piccoli di dati
- Valuta serverless (Lambda, Cloud Functions) per workload con <100 richieste/ora
Conclusioni: Architettura Sostenibile Come Pratica Standard
I pattern architetturali sostenibili non sono un lusso riservato alle grandi tech company con budget per la sostenibilità: sono buone pratiche ingegneristiche che migliorano contemporaneamente performance, costi e impatto ambientale. Tiered storage, caching efficiente, batch scheduling carbon-aware, right-sizing e query optimization producono sistemi più efficienti sotto ogni dimensione.
Il caso studio mostra che è possibile ridurre il carbon footprint di un sistema legacy del 50%+ in 6 mesi con interventi incrementali, senza dover riscrivere l'architettura da zero. La chiave è iniziare dagli interventi ad alto impatto e bassa complessità (lifecycle policies, right-sizing, query optimization), misurare i risultati con metriche SCI, e poi procedere verso ottimizzazioni più sofisticate.
La Direttiva CSRD (Corporate Sustainability Reporting Directive), in vigore per le grandi imprese europee dal 2025 e per le medie imprese dal 2026, richiederà a molte organizzazioni di rendicontare le emissioni software come parte delle emissioni Scope 3. Le architetture sostenibili non sono più solo una scelta etica: diventano un requisito normativo e competitivo.
Prossimo Articolo della Serie
Il decimo e ultimo articolo completerà la serie Green Software con ESG, CSRD e Compliance per Software Team: come strutturare il reporting obbligatorio delle emissioni software, creare audit trail per i regolatori, e integrare le metriche SCI nei processi di governance aziendale. Tratteremo anche gli strumenti pratici per il reporting: GHG Protocol, ESRS E1, e il framework Impact di GSF.
Risorse Aggiuntive
- Carbon Aware SDK (Green Software Foundation): github.com/Green-Software-Foundation/carbon-aware-sdk
- Electricity Maps API: Dati real-time intensità carbonica per 50+ regioni
- AWS Compute Optimizer: Raccomandazioni automatiche di right-sizing
- Impact Framework (GSF): Calcolo SCI per componente
- Web Almanac 2024 (HTTP Archive): Statistiche reali su performance e size web
- Cloud Carbon Footprint: Tool open source per misurare emissioni cloud multi-provider

Commenti
Caricamento commenti...
Accedi per commentare