Zrównoważone wzorce architektoniczne: przechowywanie, pamięć podręczna i wsadowe
Decyzje dotyczące architektury, które podejmujemy każdego dnia — jak organizujemy pamięć masową, jak zarządzamy pamięcią masową cache, w jaki sposób organizujemy zadania wsadowe — określ ślad węglowy oprogramowania podczas pomiaru 10-100 razy więcej w porównaniu do mikrooptymalizacji kodu. Źle zaprojektowana architektura który uzyskuje dostęp do bazy danych na każde żądanie, nie korzysta z buforowania i przetwarza dane w czasie rzeczywistym, kiedy tylko może robienie tego wsadowo może pochłonąć o rząd wielkości więcej energii niż dobrze zaprojektowana architektura.
Według analizy przeprowadzonej przez Ramy wpływu Fundacji Zielonego Oprogramowania, wybory architektoniczne związane z warstwową pamięcią masową, buforowaniem i planowaniem wsadowym reprezentują 40-60% potencjału redukcja emisji systemu oprogramowania dla przedsiębiorstwa. Nie chodzi tylko o optymalizację Techniczne: Jest to odpowiedzialność zawodowa, którą musi wziąć pod uwagę każdy architekt i deweloper integralną część zawodu.
W dziewiątym artykule z serii Green Software omówimy zrównoważone wzorce architektoniczne bardziej efektywne: od inteligentnego podziału na warstwy pamięci masowej po pamięć podręczną uwzględniającą emisję dwutlenku węgla, od planowania wsadowego po okna zielonej energii w zrównoważonym projektowaniu API. Z prawdziwymi przykładami kodu i pełnym studium przypadku co pokazuje redukcję śladu węglowego witryny e-commerce o 45% przy milionie odwiedzin dziennie.
Czego się nauczysz
- Warstwowa pamięć masowa (gorąca/ciepła/zimna/archiwum) i zasady cyklu życia danych mające na celu zmniejszenie zużycia energii
- Wzorzec buforowania uwzględniający węgiel: geointeligentny CDN, wielopoziomowe buforowanie, zoptymalizowane unieważnianie
- Przetwarzanie wsadowe w oknach zielonej energii za pomocą pakietu SDK Carbon Aware
- Właściwe dopasowywanie rozmiaru i automatyczne skalowanie w dół: jak uniknąć marnowania energii w trybie bezczynności.
- Zrównoważone wzorce baz danych: optymalizacja zapytań, zmaterializowane widoki, odczyt replik
- Wydajność sieci: kompresja, HTTP/3, przetwarzanie brzegowe
- Zrównoważone wzorce frontendu: leniwe ładowanie, optymalizacja obrazu, energia w trybie ciemnym
- Zrównoważony projekt API: paginacja, wybór pól, GraphQL vs REST
- Monitorowanie emisji dwutlenku węgla za pomocą wyników Prometheus, Grafana i SCI na usługę
- Studium przypadku e-commerce: -45% śladu węglowego przy zastosowaniu wszystkich wzorców
Seria Green Software — 10 artykułów
| # | Tytuł | Centrum | Państwo |
|---|---|---|---|
| 1 | Zasady zielonej inżynierii oprogramowania | GSF, SCI, 8 podstawowych zasad | Opublikowany |
| 2 | Pomiar emisji za pomocą CodeCarbon | CodeCarbon, profilowanie energii w Pythonie | Opublikowany |
| 3 | Carbon Aware SDK: Inteligentne planowanie | Czasowe i geograficzne przemieszczanie się obciążeń | Opublikowany |
| 4 | Climatiq API: dane dotyczące emisji w czasie rzeczywistym | Emisje API, współczynniki przeliczeniowe | Opublikowany |
| 5 | GreenOps: Zrównoważona infrastruktura jako kod | Terraform zielony, instancje punktowe, automatyczne skalowanie | Opublikowany |
| 6 | Sztuczna inteligencja i ślad węglowy: odpowiedzialne szkolenie | Modele efektywne, LoRA, kwantyzacja | Opublikowany |
| 7 | Zakres 3 w potokach oprogramowania | Emisje typu upstream/downstream, łańcuch dostaw | Opublikowany |
| 8 | Modelowanie zakresu: symulacja wpływu | Symulacja, analiza typu „co by było, gdyby”, zielony plan działania | Opublikowany |
| 9 | Zrównoważone wzorce architektoniczne | Pamięć masowa, pamięć podręczna, wsadowa, projekt API | Ten artykuł |
| 10 | ESG, CSRD i Compliance dla zespołów programistycznych | Obowiązkowe raportowanie, ścieżka audytu, wskaźniki ESG | Następny |
Warstwy pamięci masowej: właściwe dane we właściwym miejscu
Pierwszą poważną stratą energii w nowoczesnych systemach jest pamięć nie jest zoptymalizowana: rzadko dostępne dane przechowywane na wysokowydajnych dyskach SSD NVMe lub, co gorsza, w pamięci RAM. Dysk SSD przedsiębiorstwo zużywa do 6-10 W na biegu jałowym i 25 W pod obciążeniem. Zapis na dysku twardym zużywa 5-8 W. Magazynowanie taśmowe lub przechowywanie w chłodni zużywa mniej niż 0,01 W na TB w stanie bezczynności.
Strategia wielopoziomowe przechowywanie polega na klasyfikacji danych na podstawie częstotliwości dostęp i automatyczne przenoszenie ich na odpowiedni energetycznie poziom magazynowania. Organizacja Nośniki, które prawidłowo implementują wielopoziomową pamięć masową, zmniejszają koszty przechowywania 40-70% oraz powiązany ślad węglowy w podobnej proporcji.
4-warstwowa architektura pamięci masowej
| Poziom | Typ | Utajenie | Koszt/TB/miesiąc | Energia/TB | Typowe zastosowanie |
|---|---|---|---|---|---|
| Gorący | NVMe SSD/Redis | < 1 ms | 200-500 dolarów | Wysoka (25 W) | Aktywne dane z ostatnich 30 dni |
| Ciepły | Standard SSD / Standard S3 | 1-10ms | 20-50 dolarów | Średni (8W) | Dane za 1–12 miesięcy, dostęp co tydzień |
| Zimno | HDD/S3 IA/GCS Nearline | 50-250ms | 4-10 dolarów | Niski (3W) | Dane 1-7 lat, dostęp miesięczny |
| Archiwum | Lodowiec / Archiwum GCS / Taśma | 1-12 godzin | 0,40-1 USD | Minimalna (<0,01 W) | Dane > 7 lat, zgodność, kopia zapasowa |
Wdrażaj automatyczne zasady cyklu życia danych
Sercem wielowarstwowej pamięci masowej jest automatyzacja zasad cyklu życia. Zamiast zarządzać ręcznie przenosząc dane, definiujemy reguły, na podstawie których system automatycznie stosuje wiek i częstotliwość dostępu do danych.
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
}
]
}
Kompresja: najbardziej niedoceniany wzór
Kompresja danych jednocześnie zmniejsza zużycie pamięci (mniej bajtów do utrzymania) i ruch sieciowy (mniej bajtów do przesłania). Stosunek energii zaoszczędzonej do zużytej energii kompresja jest typowa 10:1 do 100:1co czyni go jednym z najpopularniejszych wzorów energetycznie korzystne.
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)
Buforowanie uwzględniające emisję gazów cieplarnianych: obsługuj więcej, obliczając mniej
Buforowanie jest prawdopodobnie najskuteczniejszym sposobem ograniczenia emisji oprogramowania: co trafienie w pamięć podręczną całkowicie eliminuje zużycie energii podczas odpowiednich obliczeń. System z współczynnik trafień pamięci podręcznej 90% wykonuje tylko 1/10 obliczeń w porównaniu do jednego bez pamięci podręcznej, z proporcjonalną oszczędnością energii.
Jednak „buforowanie uwzględniające emisję dwutlenku węgla” wykracza poza zwykłą optymalizację wydajności: rozważ theintensywność emisji dwutlenku węgla w siatce aby zdecydować, co wstępnie załadować i za ile czas przechowywania danych w pamięci podręcznej i kiedy wykonywać operacje podgrzewania pamięci podręcznej.
Architektura wielopoziomowej pamięci podręcznej
Poziomy pamięci podręcznej i wpływ energii
| Poziom | Technologia | Utajenie | Energia na uderzenie | Oszczędność energii vs DB |
|---|---|---|---|---|
| L1: W trakcie | HashMap, LRU w RAM | < 0,1 ms | ~0,001 mWh | 99,9% oszczędności |
| L2: Rozproszone | Redis, Memcached | 0,1-1 ms | ~0,01 mWh | 99% oszczędności |
| L3: Krawędź CDN | CloudFront, szybko, CF | 1-20ms | ~0,05 mWh | 95% oszczędności |
| Zapytanie DB | PostgreSQL, MySQL | 5-100ms | ~1-10 mWh | — poziom podstawowy |
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 uwzględniający emisję dwutlenku węgla: obsługa z Green Edge
Sieć CDN taka jak Cloudflare lub Fastly ma węzły brzegowe w kilkudziesięciu regionach o dużej emisji dwutlenku węgla inny. Kieruj ruch do krawędzi przy użyciu bardziej ekologicznej energii, gdy pozwala na to opóźnienie, może zmniejszyć emisję z obsługi 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 }),
});
}
}
Przetwarzanie wsadowe uwzględniające emisję dwutlenku węgla: pracuj, gdy energia jest ekologiczna
Przetwarzanie wsadowe jest idealnym kandydatem przesunięcie czasowe, zasada który polega na przenoszeniu elastycznych obciążeń w okresach zwiększonej intensywności emisji dwutlenku węgla sieci elektroenergetycznej jest niższa. System, który uruchamia swoje partie co noc, kiedy Nadwyżka energii produkowanej przez wiatr i słońce może zmniejszyć związaną z tym emisję gazów cieplarnianych 30-70% w porównaniu z realizacją w ustalonym czasie.
Paradoks partii o 02:00
Wiele systemów planuje partie o godzinie 02:00, „ponieważ jest mniejszy ruch”. Ale tam w Europie noc nie zawsze jest porą o niższej intensywności emisji dwutlenku węgla: w wielu regionach energia słoneczna i nieobecny w nocy, a wiatr jest zmienny. W niektórych obszarach 10:00–14:00 (szczyt słoneczny) lub 2:00–6:00 (stały wiatr) charakteryzują się znacznie niższą intensywnością emisji dwutlenku węgla. Wykorzystaj prawdziwe dane z Pakiet SDK Carbon Aware zamiast tymczasowej zasady.
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
)
Właściwe dopasowywanie rozmiaru i automatyczne skalowanie w dół: zatrzymanie niewidocznych odpadów
Z badania Gartnera z 2025 r. wynika, że 35-40% zasobów chmury korporacyjnej to nadmiernie zaopatrzony: serwery pracujące z 10-15% CPU, bazy danych z 90% niewykorzystanej pamięci, Lambda działa z przydzielonymi 3 GB RAM-u, gdy wystarczy 256 MB. To „bezczynne przetwarzanie” jest czysta strata energii.
Systematyczne właściwe dobieranie rozmiaru – ograniczanie zasobów do minimum niezbędnego do spełnienia wymagań wydajności — często jest to pojedyncza interwencja w najlepszy zwrot z inwestycji w energię. Nie wymaga przepisywania kodu - to kwestia konfiguracji i monitorowania.
Właściwe strategie redukcji emisji dwutlenku węgla
| Strategia | Typowe oszczędności | Złożoność | Ryzyko |
|---|---|---|---|
| Zmniejsz rozmiar instancji z nadmierną alokacją | 20-40% | Niski | Niski (łatwy powrót) |
| Agresywne automatyczne skalowanie w dół | 30-60% | Przeciętny | Średnie (opóźnienie zimnego startu) |
| Bezserwerowy do sporadycznych obciążeń | 50-90% | Wysoki | Średni (zimny start, uzależnienie od dostawcy) |
| Instancje typu Spot/Wywłaszczanie na partię | 60-80% | Wysoki | Wysoki (przerwy) |
| Zaplanuj zamknięcie po godzinach (programowanie/staging) | 40-70% | Niski | Null (środowiska inne niż produkcyjne) |
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,
}
Zrównoważone wzorce baz danych: mniej zapytań, mniej emisji
Baza danych jest często komponentem z plikiem większe zużycie energii systemu przedsiębiorstwo. Każde zapytanie obejmuje operacje we/wy dysku, alokację bufora RAM, cykle procesora do analizy, planowanie i wykonanie. Optymalizacja zapytań to nie tylko wydajność: tak jest bezpośrednią redukcję emisji.
Wzorzec 1: Zmaterializowane widoki ograniczające liczbę ponownych obliczeń
Zmaterializowane widoki są najskuteczniejszym wzorcem eliminowania kosztownych zapytań zagregowanych są stale powtarzane. Zamiast przeliczać złożone SUM, COUNT, JOIN przy każdym żądaniu, wynik jest wstępnie obliczany i aktualizowany okresowo lub za pomocą wyzwalaczy.
-- 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
Optymalizacja zapytań: problem N+1 i chętne ładowanie
Problem N+1 jest jednym z najpowszechniejszych i najbardziej kosztownych antywzorców w zakresie emisji: zamiast pojedynczego zapytania, które pobiera wszystkie niezbędne dane, uruchamiasz N+1 oddzielnych zapytań. Przy N=1000 zamówień generowanych jest 1001 zapytań zamiast 1, co mnoży ślad węglowy przez 1000 operacji.
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]
Wydajność sieci: każdy przesłany bajt wiąże się z kosztem emisji dwutlenku węgla
Przesyłanie danych wiąże się z rzeczywistym kosztem energii: 0,06-0,1 kWh na GB dla ruchu internet (szkieletowy + ostatnia mila). Aplikacja przesyłająca dziennie 10 TB nieskompresowanych danych zużywa około 600–1000 kWh na sam przesył, co odpowiada 200–400 kg CO₂ dziennie (przy średniej europejskiej intensywności 350 gCO₂/kWh).
HTTP/3 i QUIC: wydajność na poziomie protokołu
HTTP/3 z QUIC eliminuje blokowanie nagłówka linii i ogranicza wymagane podróże w obie strony nawiązać połączenia. W przypadku aplikacji z wieloma współbieżnymi żądaniami protokół HTTP/3 może zmniejszyć opóźnienie 15-30% i w konsekwencji aktywny czas procesora serwerów.
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 [];
}
Zrównoważone wzorce frontendu: klient jest częścią problemu
Frontend jest często najbardziej pomijanym elementem w analizie emisji oprogramowania, ale uruchamianie zaawansowanego JavaScriptu na milionach urządzeń ma ogromny łączny wpływ. Scenariusz 500KB JS działające na 1 milionie urządzeń zajmuje ok 50 GWh energii rocznie na urządzeniach użytkowników — nie na naszych serwerach.
Optymalizacja obrazu: Najszybsze zarobki
Obrazy przedstawiają średnio 50-70% wagi strony internetowej. Przejdź obok JPEG do WebP/AVIF zmniejsza rozmiar o 25–50%, co odpowiada zmniejszeniu transferu danych oraz w czasie dekodowania na urządzeniu użytkownika.
// 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);
}
Tryb ciemny i oszczędzanie energii na OLED
Tryb ciemny: rzeczywisty wpływ na wyświetlacze OLED
Na wyświetlaczach OLED (obecnych we wszystkich smartfonach premium i wielu laptopach od 2024 r.) czarne piksele dosłownie konsumują 0 energii (piksel OLED nie emituje światła, jeśli jest wyłączony). Interfejs z czystym czarnym tłem (#000000) na OLED może zużywać do 60-80% mniej energii wyświetlacza na białym tle (#FFFFFF). W przypadku miliardów urządzeń OLED dobrze zaimplementowany tryb ciemny stanowi redukcję znaczne emisje po stronie użytkownika.
/* 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 */
}
Zrównoważony projekt API: mniej podróży w obie strony, mniej emisji dwutlenku węgla
Projekt API ma bezpośredni wpływ na emisję: wymagają tego źle zaprojektowane interfejsy API wiele podróży w obie strony, przesyłaj więcej danych niż to konieczne i zmuszaj klientów do tego więcej próśb o uzyskanie potrzebnych informacji.
Efektywna paginacja: kursor a przesunięcie
Paginacja za pomocą OFFSET jest energetycznie kosztowna: pobranie strony 100 z 20 elementami, baza danych musi wykonać iterację i odrzucić 2000 wierszy (OFFSET 2000). Dzięki paginacji opartej na kursorze, baza danych przeskakuje bezpośrednio do właściwego punktu za pomocą indeksu, zużywając energię proporcjonalnie tylko do liczby zwróconych wierszy, a nie tych pominiętych.
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;
Monitorowanie emisji dwutlenku węgla: środki do poprawy
„Tego, czego nie da się zmierzyć, nie da się ulepszyć”. Monitoring emisji dwutlenku węgla na poziomie usługa pozwala na identyfikację najbardziej emisyjnych komponentów, śledzenie postępu w czasie, i oblicz Wynik SCI (intensywność emisji dwutlenku węgla przez oprogramowanie) jako KPI firmy.
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
Studium przypadku: Handel elektroniczny z 1 milionem wizyt dziennie — -45% śladu węglowego
Zobaczmy, jak wszystkie te wzorce integrują się w konkretnym przypadku. Handel elektroniczny z 1 milionem dzienne wizyty, 50 000 zamówień dziennie i niezoptymalizowana dotychczasowa architektura. Zespół ma wdrożył wzorce opisane w tym artykule w 6-miesięcznym projekcie. Wyniki mówią sam.
Architektura bazowa przed optymalizacją
| Część | Najpierw konfiguracja | Problem | Węgiel/miesiąc (oszacowanie) |
|---|---|---|---|
| Serwery internetowe | 20x c5.2xlarge, zawsze włączone | Średni procesor 8%, 92% bezczynności | 450 kg CO₂ |
| Bazy danych | db.r5.4xlarge, WYBIERZ * wszędzie | N+1 zapytań, bez indeksu | 280 kg CO₂ |
| Pamięć S3 | Wszystko w warstwie Standard | 5 lat dzienników, do których nigdy nie uzyskano dostępu w gorącym magazynie | 90 kg CO₂ |
| Zadania wsadowe | Naprawiono o godzinie 02:00 UTC | Brak świadomości węgla | 120 kg CO₂ |
| CDN/pamięć podręczna | Współczynnik trafień w pamięci podręcznej 35% | Zbyt krótkie TTL, brak buforowania krawędzi | 180 kg CO₂ |
| Frontend | Pakiet JS 2,8 MB, JPEG | Brak dzielenia kodu, niezoptymalizowane obrazy | 200 kg CO₂ |
| Całkowity | 1320 kg CO₂/miesiąc |
Wyniki po optymalizacji (6 miesięcy)
| Interwencja | Zastosowano wzór | Redukcja węgla | Czas wdrożenia |
|---|---|---|---|
| Automatyczne skalowanie + odpowiednie dobranie rozmiaru EC2 | Zmniejsz agresywną partię instancji punktowych | -195 kg CO₂/miesiąc (43%) | 2 tygodnie |
| Optymalizacja zapytań + zmaterializowane widoki | Poprawka N+1, projekcja, indeksy cząstkowe | -140 kg CO₂/miesiąc (50%) | 4 tygodnie |
| Zasady cyklu życia S3 | Magazynowanie warstwowe: ciepło/ciepło/zimno/archiwum | -72 kg CO₂/miesiąc (80%) | 1 tydzień |
| Planowanie partii uwzględniające emisję dwutlenku węgla | Carbon Aware SDK, zielone okna | -48 kg CO₂/miesiąc (40%) | 3 tygodnie |
| Wielopoziomowa pamięć podręczna (Redis + CDN) | Współczynnik trafień w pamięci podręcznej: 35% -> 87% | -126 kg CO₂/miesiąc (70%) | 6 tygodni |
| Frontend: WebP/AVIF + dzielenie kodu | Pakiet 2,8 MB -> 380 KB, obrazy WebP | -110 kg CO₂/miesiąc (55%) | 3 tygodnie |
| Razem zaoszczędzono | -691 kg CO₂/miesiąc (-52%) | Łącznie 6 miesięcy |
Efektem końcowym jest redukcja 52% miesięcznego śladu węglowego (od 1320 kg do 629 kg CO₂), przy rocznej oszczędności ok 8,3 tony CO₂ równoważny. To równowartość usunięcia z dróg około 4 samochodów na rok. I tak nie jest Konieczne było napisanie wniosku od nowa: wszystkie interwencje miały charakter przyrostowy.
Kluczowa lekcja ze studium przypadku
Dwie interwencje zapewniające najlepszy zwrot z inwestycji (oszczędność emisji dwutlenku węgla / wysiłek) to:
- Zasady cyklu życia S3 (obniżka 80%, 1 tydzień pracy): wystarczy kilka Linie konfiguracji infrastruktury jako kodu. Jest to wzór ze stosunkiem siły uderzenia do wysiłku najwyższy w historii.
- Automatyczne skalowanie i dopasowywanie rozmiaru (43% redukcji, 2 tygodnie): przytłaczająca Większość systemów ma nadmiar zasobów. Ogranicz alokację do niezbędnego minimum z automatycznym skalowaniem daje natychmiastowy i wymierny efekt.
Morał: zawsze zaczynaj od najprostszych wzorów. 80% zarobków pochodzi z 20%. interwencje.
Anty-wzorce, których należy unikać: 5 najczęstszych pułapek
Anty-wzorzec nr 1: Agresywne rozgrzewanie pamięci podręcznej po wyłączeniu, zielone
Masowo ładuj pamięć podręczną podczas szczytów intensywności emisji dwutlenku węgla (zwykle wieczorem, gdy nie ma energii słonecznej, a zapotrzebowanie pokrywa gaz) zużywa więcej energii niż zaoszczędź trochę. Należy zaplanować podgrzewanie pamięci podręcznej w zielonych oknach identyfikowanych przez pakiet SDK Carbon Aware.
Antywzorzec nr 2: Pełne dzienniki w magazynie Hot
Trzymanie lat dzienników debugowania w S3 Standard (gorąca warstwa) to strata pieniędzy i energii. Rzadko uzyskuje się dostęp do dzienników starszych niż 30 dni; po 90 dniach prawie nigdy. Wdrożyć zasady cyklu życia to pierwsze działanie, jakie należy podjąć w każdym starszym systemie.
Antywzorzec nr 3: Zawsze włączony w przypadku sporadycznych obciążeń
Usługa, która otrzymuje 10 żądań dziennie, nie musi działać 24 godziny na dobę, 7 dni w tygodniu na dedykowanej instancji. Lambda, Cloud Run lub Fargate ze skalą do zera to ekologiczny wybór w przypadku sporadycznych obciążeń. Zawsze włączona instancja EC2 t3.small emituje ~15 kg CO₂/miesiąc; Lambda na 10 żądań dziennie emituje <0,001 kg CO₂/miesiąc. 15 000 razy mniej.
Anty-wzorzec nr 4: WYBIERZ * w produkcji
SELECT * przenosi wszystkie pola tabeli nawet wtedy, gdy aplikacja ich używa
tylko 2-3. W tabelach zawierających ponad 50 kolumn i miliony wierszy powoduje to pomnożenie danych przez 10–20
przeniesione z bazy danych, z równoważnym wpływem energetycznym. Zawsze używaj wyraźnej projekcji.
Antywzorzec nr 5: Monolityczny pakiet JavaScript
Pobranie i przeglądanie pakietu JS o wielkości 3MB przez 1 milion użytkowników dziennie zajmuje ok 2000 kWh energii procesora na urządzeniach użytkowników. Dzięki dzieleniu kodu i leniwemu ładowaniu plik Początkowy pakiet może spaść do 100–200 KB, zmniejszając zużycie energii o 15–30 razy strona klienta. Energia zużywana na urządzeniach użytkowników jest częścią Zakresu 3 oprogramowania.
Lista kontrolna: Zrównoważone wzorce Twojego następnego sprintu
Wysoki priorytet (maksymalny wpływ, niska złożoność)
- Zaimplementuj zasady cyklu życia S3/GCS, aby automatycznie przenosić dane do zimnych warstw
- Analizuj i ograniczaj udostępnianie instancji EC2/GCE (AWS Compute Optimizer, GCP Recommender)
- Dodaj harmonogram zamykania środowisk programistycznych/pomostowych poza godzinami pracy
- Włącz kompresję gzip/brotli na wszystkich punktach końcowych HTTP (jeśli nie została jeszcze włączona)
- Konwertuj obrazy na WebP/AVIF w potoku kompilacji
- Usuń wszystkie
SELECT *zastępując je wyraźną projekcją
Średni priorytet (duży wpływ, umiarkowana złożoność)
- Zaimplementuj wielopoziomowe buforowanie (w procesie L1 + L2 Redis) dla najczęściej używanych zasobów
- Twórz zmaterializowane widoki dla często wykonywanych zapytań zbiorczych
- Przeprowadź migrację elastycznych czasowo zadań wsadowych do planowania Carbon Aware SDK
- Rozwiązuj problemy N+1 z szybkim ładowaniem lub DataLoaderem (GraphQL)
- Implementuje paginację opartą na kursorach jako zamiennik paginacji offsetowej
- Dodaj odpowiednie znaczniki ETag i kontrolki pamięci podręcznej do odpowiedzi API
Niski priorytet (stopniowe ulepszenia)
- Rozważ migrację do protokołu HTTP/3 w przypadku aplikacji wrażliwych na opóźnienia
- Zaimplementuj wybór pól w punktach końcowych REST (parametr zapytania
?fields=) - Dodaj tryb ciemny z prawdziwą czernią, aby zmniejszyć zużycie energii na urządzeniach OLED użytkowników
- Skonfiguruj monitorowanie emisji dwutlenku węgla za pomocą pulpitu nawigacyjnego Prometheus + Grafana w celu uzyskania wyniku SCI
- Wprowadź indeksy częściowe dla filtrowanych zapytań dotyczących małych podzbiorów danych
- Oceń rozwiązania bezserwerowe (Lambda, Cloud Functions) pod kątem obciążeń <100 żądań na godzinę
Wnioski: Zrównoważona architektura jako standardowa praktyka
Zrównoważone wzorce architektoniczne nie są luksusem zarezerwowanym dla dużych firm technologicznych z ograniczonym budżetem dla zrównoważonego rozwoju: są to dobre praktyki inżynieryjne, które jednocześnie się poprawiają wydajność, koszty i wpływ na środowisko. Warstwowa pamięć masowa, wydajne buforowanie, praca wsadowa Planowanie uwzględniające emisję gazów cieplarnianych, odpowiednie dobieranie rozmiaru i optymalizacja zapytań tworzą bardziej wydajne systemy pod każdym wymiarem.
Studium przypadku pokazuje, że możliwe jest zmniejszenie śladu węglowego starszego systemu 50%+ w ciągu 6 miesięcy przy stopniowych interwencjach, bez konieczności przepisywania architektury od zera. Klucz polega na rozpoczęciu od interwencji o dużym wpływie i niskiej złożoności (polityki cyklu życia, odpowiedni rozmiar, optymalizacja zapytań), zmierz wyniki za pomocą metryk SCI, a następnie przejdź dalej bardziej wyrafinowane optymalizacje.
La Dyrektywa CSRD (Dyrektywa w sprawie raportowania zrównoważonego rozwoju przedsiębiorstw), obowiązująca od dużych europejskich przedsiębiorstw od 2025 r., a dla średnich przedsiębiorstw od 2026 r., będą wymagały wielu organizacje do zgłaszania emisji oprogramowania w ramach emisji Zakresu 3 zrównoważona architektura nie jest już tylko wyborem etycznym: staje się wymogiem regulacyjne i konkurencyjne.
Następny artykuł z serii
Dziesiąty i ostatni artykuł zakończy serię Green Software ESG, CSRD i Compliance dla zespołów programistycznych: jak ustrukturyzować raportowanie obowiązkowych wersji oprogramowania, tworzyć ścieżki audytu dla organów regulacyjnych i integrować Metryki SCI w procesach ładu korporacyjnego. Omówimy także praktyczne narzędzia w zakresie raportowania: Protokół GHG, ESRS E1 i ramy oddziaływania GSF.
Dodatkowe zasoby
- Pakiet SDK uwzględniający emisję dwutlenku węgla (Fundacja Zielonego Oprogramowania): github.com/Green-Software-Foundation/carbon-aware-sdk
- Interfejs API map energii elektrycznej: Dane dotyczące intensywności emisji dwutlenku węgla w czasie rzeczywistym dla ponad 50 regionów
- Optymalizator obliczeniowy AWS: Automatyczne zalecenia dotyczące prawidłowego rozmiaru
- Ramy wpływu (GSF): Obliczenie SCI według składnika
- Almanach sieciowy 2024 (Archiwum HTTP): Prawdziwe statystyki dotyczące wydajności i rozmiaru sieci
- Ślad węglowy w chmurze: Narzędzie typu open source do pomiaru emisji z chmury wielu dostawców







