持続可能なアーキテクチャ パターン: ストレージ、キャッシュ、バッチ
私たちが毎日行うアーキテクチャ上の決定 - ストレージをどのように構築するか、ストレージをどのように管理するか キャッシュ、バッチ ジョブを調整する方法 - を決定する 測定におけるソフトウェアの二酸化炭素排出量 10~100倍 コードの微細な最適化と比較して。設計が不十分なアーキテクチャ リクエストごとにデータベースにアクセスし、キャッシュを使用せず、可能であればリアルタイムでデータを処理します。 バッチで実行すると、適切に設計されたアーキテクチャよりも桁違いに多くのエネルギーを消費する可能性があります。
による分析によると、 グリーン ソフトウェア財団インパクト フレームワーク、アーキテクチャ上の選択 ストレージ階層化、キャッシュ、バッチ スケジューリングに関連するものは、 ポテンシャルの 40 ~ 60% 排出量の削減 エンタープライズ ソフトウェア システムの。最適化だけではありません 技術: すべてのアーキテクトと開発者が考慮する必要がある専門的な責任です。 自分の職業の不可欠な部分。
Green Software シリーズの 9 回目の記事では、 持続可能な建築パターン より効果的: インテリジェントなストレージ階層化からカーボンアウェアキャッシュまで、バッチスケジューリングから API の持続可能な設計におけるグリーン エネルギー ウィンドウ。実際のコード例と完全なケーススタディ付き これは、毎日 100 万回の訪問がある電子商取引サイトの二酸化炭素排出量が 45% 削減されることを示しています。
何を学ぶか
- エネルギーを削減するための階層型ストレージ (ホット/ウォーム/コールド/アーカイブ) とデータ ライフサイクル ポリシー
- カーボン認識キャッシュ パターン: 地理インテリジェント CDN、マルチレベル キャッシュ、最適化された無効化
- Carbon Aware SDK を使用したグリーン エネルギー ウィンドウでのバッチ処理
- 適切なサイジングと自動スケールダウン: エネルギーの無駄な「アイドル コンピューティング」を回避する方法。
- 持続可能なデータベース パターン: クエリの最適化、マテリアライズド ビュー、リード レプリカ
- ネットワーク効率: 圧縮、HTTP/3、エッジ コンピューティング
- 持続可能なフロントエンド パターン: 遅延読み込み、画像の最適化、ダーク モードのエネルギー
- 持続可能な API 設計: ページネーション、フィールド選択、GraphQL と REST
- Prometheus、Grafana、およびサービスごとの SCI スコアによる炭素モニタリング
- E コマースのケーススタディ: すべてのパターンを適用すると二酸化炭素排出量が 45% 削減
グリーン ソフトウェア シリーズ — 10 件の記事
| # | タイトル | 集中 | Stato |
|---|---|---|---|
| 1 | グリーン ソフトウェア エンジニアリングの原則 | GSF、SCI、8つの基本原則 | 発行済み |
| 2 | CodeCarbon による排出量の測定 | CodeCarbon、Python エネルギー プロファイリング | 発行済み |
| 3 | Carbon Aware SDK: インテリジェントなスケジューリング | ワークロードの時間的および地理的移動 | 発行済み |
| 4 | Climatiq API: リアルタイム排出データ | API排出量、換算係数 | 発行済み |
| 5 | GreenOps: コードとしての持続可能なインフラストラクチャ | Terraform グリーン、スポット インスタンス、自動スケーリング | 発行済み |
| 6 | AI と二酸化炭素排出量: 責任あるトレーニング | 効率的なモデル、LoRA、量子化 | 発行済み |
| 7 | ソフトウェア パイプラインのスコープ 3 | 上流/下流の排出量、サプライチェーン | 発行済み |
| 8 | スコープ モデリング: 影響のシミュレーション | シミュレーション、what-if 分析、グリーンロードマップ | 発行済み |
| 9 | 持続可能な建築パターン | ストレージ、キャッシュ、バッチ、API 設計 | この記事 |
| 10 | ソフトウェアチームの ESG、CSRD、コンプライアンス | 必須のレポート、監査証跡、ESG指標 | Prossimo |
ストレージ階層化: 適切なデータを適切な場所に
現代のシステムにおける最初の主要なエネルギー浪費は、 ストレージが最適化されていない: 高性能 NVMe SSD に存在するデータ、またはさらに悪いことに、RAM メモリに存在するデータにはほとんどアクセスされません。 SSD 企業は最大で消費します アイドル時 6 ~ 10 ワット、負荷時 25 ワット。 HDDに保存 5〜8ワットを消費します。テープ ストレージまたはコールド ストレージのアイドル時の消費電力は 1 TB あたり 0.01 ワット未満です。
の戦略 階層型ストレージ 頻度に基づいてデータを分類する にアクセスし、エネルギーに適したストレージ階層に自動的に移動します。組織 階層型ストレージを適切に実装したメディアによりストレージ コストが削減されます 40-70% 関連する二酸化炭素排出量も同様の割合で見られます。
4層ストレージアーキテクチャ
| レベル | タイプ | レイテンシ | コスト/TB/月 | エネルギー/TB | 一般的な使用方法 |
|---|---|---|---|---|---|
| 熱い | NVMe SSD / Redis | < 1ms | 200~500ドル | 高 (25W) | 過去 30 日間のアクティブ データ |
| 暖かい | SSD規格 / S3規格 | 1~10ミリ秒 | 20~50ドル | 中(8W) | データ 1 ~ 12 か月、毎週アクセス |
| 寒い | HDD/S3 IA/GCS ニアライン | 50~250ミリ秒 | 4〜10ドル | 低(3W) | データ 1 ~ 7 年、毎月アクセス |
| アーカイブ | 氷河 / GCS アーカイブ / テープ | 1~12時間 | $0.40-1 | 最小 (<0.01W) | データ > 7 年、コンプライアンス、バックアップ |
自動データライフサイクルポリシーを実装する
階層型ストレージの中心は、ライフサイクル ポリシーの自動化です。管理する代わりに データを手動で移動する場合は、システムが自動的に適用するルールを定義します。 データアクセスの年齢と頻度。
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
}
]
}
圧縮: 最も過小評価されているパターン
データ圧縮により、同時にストレージ消費量が削減され(維持するバイト数が減り)、 ネットワーク トラフィック (転送バイト数が少なくなります)。節約されたエネルギーと消費されたエネルギーの比率 圧縮は通常、 10:1 ~ 100:1、最も人気のあるパターンの 1 つになります。 エネルギー的に有利。
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)
カーボンを意識したキャッシュ: コンピューティングを減らしてより多くのサービスを提供
キャッシュはおそらく、ソフトウェアの排出量を削減するための最も強力なパターンです。 キャッシュ ヒットにより、対応する計算のエネルギー消費が完全に排除されます。を備えたシステム キャッシュヒット率90% キャッシュなしの場合と比較して、計算の実行量は 1/10 のみです。 比例してエネルギーも節約されます。
しかし、「カーボンを意識したキャッシュ」は、単純なパフォーマンスの最適化を超えています。 のグリッドの炭素強度 何をどれくらいプリロードするかを決める データをキャッシュに保持する時間、およびキャッシュ ウォーミング操作を実行するタイミング。
マルチレベルキャッシュアーキテクチャ
キャッシュレベルとエネルギーへの影響
| レベル | テクノロジー | レイテンシ | ヒットのためのエネルギー | 省エネとDBの比較 |
|---|---|---|---|---|
| L1: 処理中 | ハッシュマップ、RAM 内の LRU | < 0.1ms | ~0.001mWh | 99.9% 節約 |
| L2: 分散型 | Redis、Memcached | 0.1~1ms | ~0.01mWh | 99% 節約 |
| L3: CDN エッジ | CloudFront、Fastly、CF | 1~20ミリ秒 | ~0.05mWh | 95% 節約 |
| DBクエリ | PostgreSQL、MySQL | 5~100ミリ秒 | ~1~10mWh | — ベースライン |
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: グリーンエッジからサービスを提供
Cloudflare や Fastly のような CDN は、数十の非常に炭素集約的な地域にエッジ ノードを持っています 違う。遅延が許容される場合は、より環境に優しいエネルギーを使用してトラフィックをエッジにルーティングします。 サービスによる排出量を削減できる 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 }),
});
}
}
カーボンを意識したバッチ処理: エネルギーがグリーンなときに機能します
バッチ処理は次の用途に最適です。 一時的なシフト、原則 これは、炭素濃度が高いときに柔軟なワークロードを移動することで構成されます。 電力網のレベルが低くなります。バッチを毎晩実行するシステムは、 風力と太陽光発電による余剰エネルギーは、それに伴う排出量を削減できます。 30-70% 固定時間実行と比較して。
バッチパラドックス 02:00
多くのシステムは、「トラフィックが少ないため」02:00 にバッチをスケジュールします。しかし、ヨーロッパでは、 夜は必ずしも炭素強度が低い時間であるとは限りません。多くの地域では、太陽エネルギーと 夜は不在で風も変わります。一部の地域では、午前10時から午後2時(太陽のピーク)または午前2時から午前6時 (一定の風)炭素強度ははるかに低くなります。実際のデータを使用する 一時的な経験則ではなく、Carbon Aware SDK。
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
)
適切なサイジングと自動スケールダウン: 目に見えない無駄をなくす
2025 年の Gartner の調査では、 エンタープライズ クラウド リソースの 35 ~ 40% は 過剰にプロビジョニングされた: 10 ~ 15% の CPU で実行されているサーバー、90% の未使用メモリのあるデータベース、 256MB で十分な場合、Lambda は 3GB の RAM を割り当てて機能します。この「アイドルコンピューティング」とは、 純粋なエネルギーの無駄遣い。
体系的な適切なサイジング — 要件を満たすために必要な最小限にリソースを削減します。 パフォーマンスの向上 - 多くの場合、それは、 最高のエネルギーROI。 コードを書き直す必要はありません。設定と監視の問題です。
炭素削減のための適切な規模の戦略
| 戦略 | 一般的な節約額 | 複雑 | リスク |
|---|---|---|---|
| 過剰にプロビジョニングされたインスタンスのサイズを削減する | 20-40% | 低い | 低 (簡単なロールバック) |
| 積極的な自動スケールダウン | 30-60% | 平均 | 中 (コールド スタートの待ち時間) |
| サーバーレスで断続的なワークロードに対応 | 50-90% | 高い | 中 (コールド スタート、ベンダー ロックイン) |
| バッチごとのスポット/プリエンプティブル インスタンス | 60-80% | 高い | 高 (中断) |
| 時間外にシャットダウンをスケジュールする (開発/ステージング) | 40-70% | 低い | Null (非本番環境) |
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,
}
持続可能なデータベース パターン: クエリの数を減らし、二酸化炭素を削減
データベースは多くの場合、 より多くのエネルギー消費 システムの 企業。各クエリには、ディスク I/O、RAM バッファ割り当て、解析のための CPU サイクル、 計画と実行。クエリの最適化はパフォーマンスだけではありません。 排出量の直接的な削減。
パターン 1: 再計算を減らすためのマテリアライズド ビュー
マテリアライズド ビューは、コストのかかる集計クエリを排除するための最も効果的なパターンです。 継続的に再実行されます。リクエストごとに複雑な SUM、COUNT、JOIN を再計算する代わりに、 結果は事前に計算され、定期的にまたはトリガーによって更新されます。
-- 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
クエリの最適化: N+1 問題と積極的な読み込み
N+1 問題は、排出量に関して最も一般的でコストのかかるアンチパターンの 1 つです。 必要なデータをすべて取得する単一のクエリの代わりに、N+1 個の個別のクエリを実行します。 N=1000 件の注文の場合、クエリは 1 つではなく 1001 つ生成され、二酸化炭素排出量は 1000 倍になります。 手術の様子。
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]
ネットワーク効率: 転送される各バイトには炭素コストがかかります
データの転送には実際のエネルギーコストがかかります。 0.06 ~ 0.1 kWh/GB 交通用 インターネット (バックボーン + ラスト マイル)。 1 日あたり 10TB の非圧縮データを転送するアプリケーション 送電だけで約 600 ~ 1000 kWh を消費し、1 日あたり 200 ~ 400 kg CO₂ に相当します。 (ヨーロッパの平均原単位は 350 gCO₂/kWh)。
HTTP/3 と QUIC: プロトコル レベルの効率
QUIC を使用した HTTP/3 は、行頭のブロックを排除し、通信に必要なラウンドトリップを削減します。 接続を確立します。多くの同時リクエストがあるアプリケーションの場合、HTTP/3 により、 のレイテンシ 15-30% したがって、サーバーのアクティブな CPU 時間も増加します。
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 [];
}
持続可能なフロントエンド パターン: クライアントも問題の一部です
フロントエンドは、ソフトウェア排出量分析において最も見落とされがちなコンポーネントです。 しかし、数百万台のデバイスで負荷の高い JavaScript を実行すると、総合的に大きな影響が生じます。スクリプト 100 万台のデバイスで 500KB の JS を実行するには約 年間エネルギー量 50 GWh 当社のサーバーではなく、ユーザーのデバイス上で。
画像の最適化: 最速の収益
画像は平均して次のことを表しています。 重量の50~70% ウェブページの。通り過ぎます JPEG を WebP/AVIF に変換すると、サイズが 25 ~ 50% 削減され、データ転送も同等に削減されます。 ユーザーのデバイスでのデコード時間も同様です。
// 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);
}
OLEDのダークモードと省エネ
ダークモード: OLED ディスプレイへの実際の影響
OLED ディスプレイ (2024 年以降、すべての高級スマートフォンと多くのラップトップに搭載) では、黒いピクセルが発生します。 彼らは文字通り消費します エネルギー0 (オフの場合、OLED ピクセルは発光しません)。 OLED 上の純粋な黒の背景 (#000000) のインターフェイスは、最大で エネルギーを 60 ~ 80% 削減 白い背景に対するディスプレイの表示 (#FFFFFF)。 何十億もの OLED デバイスがあるため、適切に実装されたダーク モードを提供することでコストを削減できます。 重大なユーザー側の排出量。
/* 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 の持続可能な設計: 往復の削減、二酸化炭素の削減
API 設計は排出量に直接影響します。設計が不十分な API では、 複数回の往復、必要以上のデータの転送、およびクライアントへの強制 必要な情報を取得するためにさらに多くのリクエストが発生します。
効率的なページネーション: カーソルとオフセット
OFFSET を使用したページネーションはエネルギー的にコストがかかります。20 要素を持つ 100 ページを取得するには、 データベースは 2000 行 (OFFSET 2000) を反復して破棄する必要があります。カーソルベースのページネーションでは、データベース インデックスを使用して正しいポイントに直接ジャンプし、その数にのみ比例するエネルギーを消費します スキップされた行ではなく、返された行の数。
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;
炭素モニタリング: 改善策
「測定できないものは改善できない。」レベルでの炭素モニタリング このサービスを使用すると、最も放射性の高いコンポーネントを特定し、時間の経過とともに進行状況を追跡できます。 そして計算します SCI スコア (ソフトウェア炭素強度) 企業のKPIとして。
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
ケーススタディ: 1 日あたりの訪問数が 100 万の電子商取引 — 二酸化炭素排出量が 45% 削減
これらすべてのパターンが具体的なケースでどのように統合されるかを見てみましょう。 100万人が利用する電子商取引 毎日の訪問、1 日あたり 50,000 件の注文、そして最適化されていないレガシー アーキテクチャ。チームは この記事で説明されているパターンを 6 か月のプロジェクトで実装しました。結果が物語る 一人で。
最適化前のアーキテクチャベースライン
| 成分 | 最初に設定を行う | 問題 | 炭素/月 (推定) |
|---|---|---|---|
| Webサーバー | 20x c5.2xlarge 常時オン | 平均 CPU 8%、アイドル状態 92% | 450kg CO₂ |
| データベース | db.r5.4xlarge、どこでも SELECT * | N+1 クエリ、インデックスなし | 280kg CO₂ |
| S3ストレージ | すべてスタンダードレベル | ホット ストレージ内の 5 年間のログにはアクセスされませんでした | 90kg CO₂ |
| バッチジョブ | 02:00 UTC に固定 | 二酸化炭素の意識がない | 120kg CO₂ |
| CDN/キャッシュ | キャッシュヒット率35% | TTLが短すぎる、エッジキャッシュがない | 180kg CO₂ |
| フロントエンド | バンドル JS 2.8MB、JPEG | コード分割なし、画像は最適化されていない | 200kg CO₂ |
| 合計 | 1,320 kg CO₂/月 |
最適化後の結果 (6 か月)
| 介入 | 適用されたパターン | 炭素削減 | 実施時間 |
|---|---|---|---|
| 自動スケーリング + EC2 の適切なサイジング | 積極的なスポットインスタンスのバッチをスケールダウンする | -195 kg CO₂/月 (43%) | 2週間 |
| クエリの最適化 + マテリアライズド ビュー | N+1 修正、射影、部分インデックス | -140 kg CO₂/月 (50%) | 4週間 |
| S3 ライフサイクル ポリシー | 階層型ストレージ: ホット/ウォーム/コールド/アーカイブ | -72 kg CO₂/月 (80%) | 1週間 |
| カーボンを意識したバッチ スケジューリング | Carbon Aware SDK、グリーンウィンドウ | -48 kg CO₂/月 (40%) | 3週間 |
| マルチレベルキャッシュ (Redis + CDN) | キャッシュヒット率: 35% -> 87% | -126 kg CO₂/月 (70%) | 6週間 |
| フロントエンド: WebP/AVIF + コード分割 | バンドル 2.8MB -> 380KB、WebP 画像 | -110 kg CO₂/月 (55%) | 3週間 |
| 合計節約額 | -691 kg CO₂/月 (-52%) | 合計6か月 |
最終的な結果は次のとおりです。 月間二酸化炭素排出量の 52% (CO₂ 1,320 kg から 629 kg)、年間約 8.3トンのCO₂ 同等の。これは、約 4 台の車が 1 年間道路を離れることに相当します。そしてそうではありません アプリケーションを最初から書き直す必要がありました。すべての介入は段階的でした。
ケーススタディから得た重要な教訓
最高の ROI (削減された炭素数 / 労力) が得られた 2 つの介入は次のとおりです。
- S3 ライフサイクル ポリシー (80% 削減、1 週間の作業): 数個で十分です Infrastructure-as-Code 構成行。インパクト/エフォート比のあるパターンです 過去最高。
- 自動スケーリングと適切なサイズ調整 (43% 削減、2 週間): 圧倒的なもの ほとんどのシステムはオーバープロビジョニングされています。プロビジョニングを必要最小限に減らす 自動スケーリングを使用すると、即時に測定可能な影響が生じます。
教訓: 常に最も単純なパターンから始めてください。収益の 80% は 20% から得られます。 介入。
避けるべきアンチパターン: 最も一般的な 5 つの落とし穴
アンチパターン #1: 積極的なオフタイム キャッシュのウォーミング グリーン
炭素強度のピーク時(通常は夕方、 太陽光発電がなく、ガスが需要を賄っている場合)より多くのエネルギーを消費します。 いくつか保存してください。キャッシュのウォーミングは、Carbon Aware SDK によって識別されるグリーン ウィンドウでスケジュールする必要があります。
アンチパターン #2: ホット ストレージ内の詳細ログ
S3 Standard (ホット層) に何年ものデバッグ ログを保存しておくのは、お金とエネルギーの無駄です。 30 日より古いログにはほとんどアクセスされません。 90日後はほとんどありません。実装する ライフサイクル ポリシーは、レガシー システムで最初に実行するアクションです。
アンチパターン #3: 断続的なワークロードに対して常時稼働
1 日あたり 10 件のリクエストを受け取るサービスは、専用インスタンスで 24 時間年中無休で実行する必要はありません。 断続的なワークロードには、ゼロスケールの Lambda、Cloud Run、または Fargate が環境に優しい選択肢です。 常時稼働の EC2 t3.small インスタンスは、毎月最大 15 kg の CO₂ を排出します。 1 日あたり 10 リクエストの Lambda 二酸化炭素排出量は月あたり 0.001 kg 未満です。 15,000分の1以下です。
アンチパターン #4: 本番環境での SELECT *
SELECT * アプリケーションがテーブルのフィールドを使用している場合でも、テーブルのすべてのフィールドを転送します。
2-3だけ。 50 以上の列と数百万の行があるテーブルでは、データが 10 ~ 20 倍になります。
データベースから転送され、同等のエネルギー影響を及ぼします。常に明示的な投影を使用してください。
アンチパターン #5: モノリシック JavaScript バンドル
1 日に 100 万人のユーザーが 3MB の JS バンドルをダウンロードして表示するには、約 ユーザーデバイスの CPU エネルギーは 2,000 kWh。コード分割と遅延読み込みにより、 初期バンドルは 100 ~ 200KB まで削減でき、消費電力を 15 ~ 30 分の 1 に削減できます。 クライアント側。ユーザーのデバイスで消費されるエネルギーはスコープ 3 の一部です ソフトウェアの。
チェックリスト: 次のスプリントのための持続可能なパターン
優先度が高い (影響は最大、複雑さは低い)
- S3/GCS ライフサイクル ポリシーを実装して、データをコールド層に自動的に移動する
- EC2/GCE インスタンスのプロビジョニングを分析して削減します (AWS Compute Optimizer、GCP Recommender)
- 営業時間外の開発/ステージング環境のシャットダウン スケジュールを追加
- すべての HTTP エンドポイントで gzip/brotli 圧縮を有効にします (まだ有効になっていない場合)。
- ビルド パイプラインで画像を WebP/AVIF に変換する
- すべて削除する
SELECT *それらを明示的な投影に置き換えます
優先度中 (影響度は高く、複雑さは中程度)
- 最もアクセスされるリソースに対してマルチレベル キャッシュ (L1 インプロセス + L2 Redis) を実装します。
- 頻繁に実行される集計クエリ用にマテリアライズド ビューを作成する
- 時間に柔軟なバッチ ジョブを Carbon Aware SDK スケジューリングに移行する
- 積極的な読み込みまたは DataLoader (GraphQL) を使用して N+1 の問題を解決する
- オフセット ページネーションの代わりにカーソル ベースのページネーションを実装します。
- 適切な ETag と Cache-Control を API 応答に追加します
優先度が低い (段階的な改善)
- 遅延に敏感なアプリケーションの場合は、HTTP/3 への移行を検討してください。
- REST エンドポイントにフィールド選択を実装します (クエリ パラメータ)
?fields=) - 真の黒を使用したダークモードを追加して、ユーザーの OLED デバイスの消費電力を削減します。
- SCI スコア用の Prometheus + Grafana ダッシュボードを使用して炭素モニタリングを構成する
- データの小さなサブセットに対するフィルタリングされたクエリに部分インデックスを導入します。
- 1 時間あたり 100 リクエスト未満のワークロードのサーバーレス (Lambda、Cloud Functions) を評価する
結論: 標準的な実践としての持続可能な建築
持続可能なアーキテクチャ パターンは、予算のある大手テクノロジー企業だけの贅沢品ではありません 持続可能性のために:それらは同時に改善する優れたエンジニアリング実践です パフォーマンス、コスト、環境への影響。階層型ストレージ、効率的なキャッシュ、バッチ カーボンを意識したスケジューリング、適切なサイジング、クエリの最適化により、より効率的なシステムが生成されます あらゆる次元の下で。
このケーススタディは、レガシー システムの二酸化炭素排出量を削減できることを示しています アーキテクチャを最初から書き直す必要がなく、段階的な介入により 6 か月で 50% 以上。鍵 影響力が大きく複雑性の低い介入から始めることです(ライフサイクル政策、 適切なサイジング、クエリの最適化など)、SCI メトリクスで結果を測定し、次のステップに進みます。 より洗練された最適化。
La CSRD指令 (企業の持続可能性報告指令)、2018 年に施行 欧州の大企業は 2025 年から、中堅企業は 2026 年から、多くの 組織はソフトウェア排出量をスコープ 3 排出量の一部として報告する必要がある 持続可能な建築はもはや単なる倫理的な選択ではなく、要件となります 規制と競争。
シリーズの次の記事
最後の 10 回目の記事では、Green Software シリーズを完了します。 ソフトウェアチームの ESG、CSRD、コンプライアンス: レポートを構成する方法 必須のソフトウェア リリース、規制当局向けの監査証跡の作成、および コーポレートガバナンスプロセスにおけるSCIメトリクス。実用的なツールも紹介します レポート用: GHG プロトコル、ESRS E1、および GSF Impact フレームワーク。
追加リソース
- カーボンアウェア SDK (グリーン ソフトウェア財団): github.com/Green-Software-Foundation/carbon-aware-sdk
- 電力マップ API: 50 以上の地域のリアルタイム炭素強度データ
- AWS コンピューティング オプティマイザー: 適切なサイジングの自動推奨
- インパクトフレームワーク (GSF): コンポーネント別の SCI 計算
- ウェブ年鑑 2024 (HTTP アーカイブ): Web パフォーマンスとサイズに関する実際の統計
- クラウドの二酸化炭素排出量: マルチプロバイダーのクラウド排出量を測定するためのオープンソース ツール







