第一章:Go云原生网络基建全景概览
Go语言凭借其轻量级并发模型、静态编译、低延迟网络栈和丰富的标准库,已成为构建云原生网络基础设施的首选语言。从服务网格的数据平面(如Envoy扩展、Linkerd-proxy插件)到API网关(Kong Go Plugins、Krakend)、从自定义Ingress控制器到eBPF辅助的可观测代理,Go正深度嵌入现代云网络的每一层。
核心能力支柱
- net/http 与 net:提供零依赖HTTP/1.1、HTTP/2服务器及底层TCP/UDP连接管理,支持连接复用、超时控制与上下文取消;
- goroutine + channel:天然适配高并发网络场景,单机轻松支撑数万长连接;
- go net/http/httputil:可组合反向代理中间件,无需第三方框架即可构建动态路由、熔断与重试逻辑;
- 标准库 net/url、net/http/cgi、crypto/tls:支撑安全传输、URL解析与网关协议桥接。
典型网络组件形态
| 组件类型 | Go实现代表 | 关键特性 |
|---|---|---|
| API网关 | Kong Go Plugin、Tyk Gateway (Go版) | 插件热加载、JWT验证、限流策略链式执行 |
| 服务发现客户端 | Consul API、etcd/client/v3 | Watch机制+KeepAlive会话保活 |
| eBPF协同代理 | Cilium Agent(核心用Go编写) | 通过cilium/ebpf库加载XDP程序,实现L3/L4加速 |
快速启动一个基础HTTP代理示例
以下代码演示如何用标准库构建可扩展的反向代理,支持路径重写与请求头注入:
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// 目标后端地址
backend, _ := url.Parse("http://127.0.0.1:8080")
proxy := httputil.NewSingleHostReverseProxy(backend)
// 自定义Director:修改请求目标与Header
proxy.Director = func(req *http.Request) {
req.URL.Scheme = backend.Scheme
req.URL.Host = backend.Host
req.Header.Set("X-Forwarded-By", "go-native-proxy") // 注入追踪头
}
log.Println("Starting proxy on :8081")
log.Fatal(http.ListenAndServe(":8081", proxy))
}
运行该程序后,所有发往 http://localhost:8081/api/users 的请求将被透明转发至 http://127.0.0.1:8080/api/users,并携带自定义标识头。此模式可无缝集成Prometheus指标暴露、OpenTelemetry链路注入等云原生可观测能力。
第二章:eBPF深度集成的Go网络项目解析
2.1 eBPF程序生命周期管理与Go绑定原理
eBPF程序从加载到卸载需经历严格的状态流转,Go生态通过cilium/ebpf库实现安全绑定。
生命周期关键阶段
- 加载(Load):验证器校验指令安全性,分配内核资源
- 附加(Attach):挂载至钩子点(如
kprobe、cgroup) - 运行(Run):由内核JIT编译执行,受
RLIMIT_MEMLOCK约束 - 卸载(Close):自动触发
bpf_program__close()释放内存与引用计数
Go绑定核心机制
prog := obj.MyTracepoint // *ebpf.Program,封装fd与元数据
link, err := prog.Attach(&ebpf.TracepointOptions{
Section: "syscalls/sys_enter_openat",
})
此代码调用
bpf_prog_load_xattr()系统调用,Section指定ELF节名以匹配内核符号;link持有文件描述符与自动清理逻辑,确保defer时调用link.Close()。
| 阶段 | Go方法 | 内核系统调用 |
|---|---|---|
| 加载 | ebpf.LoadCollection() |
bpf(BPF_PROG_LOAD, ...) |
| 附加 | prog.Attach() |
bpf(BPF_PROG_ATTACH, ...) |
| 卸载 | link.Close() |
close(fd) + 自动detach |
graph TD
A[Go程序调用ebpf.LoadCollection] --> B[内核验证指令合法性]
B --> C[分配prog_fd并JIT编译]
C --> D[Attach至tracepoint]
D --> E[事件触发时执行eBPF字节码]
2.2 基于libbpf-go的高性能数据面开发实践
libbpf-go 将 eBPF 程序加载、映射管理与事件处理封装为 Go 原生接口,显著降低数据面开发门槛。
核心优势对比
| 特性 | cgo + libbpf(C) | libbpf-go |
|---|---|---|
| 内存安全 | ❌(手动管理) | ✅(GC 自动回收) |
| Map 操作语法 | verbose | map.Lookup(key) |
| 事件回调集成 | 需轮询/epoll | link.AttachXDP() |
eBPF 程序加载示例
obj := &ebpf.ProgramSpec{
Type: ebpf.XDP,
Instructions: progInstructions,
License: "Dual MIT/GPL",
}
prog, err := ebpf.NewProgram(obj)
if err != nil {
log.Fatal("failed to load XDP program:", err)
}
// Attach to interface 'eth0' with flags XDP_FLAGS_UPDATE_IF_NOEXIST
link, err := prog.AttachXDP("eth0")
AttachXDP 底层调用 bpf_link_create(),自动处理内核版本兼容性;XDP_FLAGS_UPDATE_IF_NOEXIST 避免重复挂载冲突。
数据同步机制
- 使用
perf.Reader实时消费内核侧bpf_perf_event_output()推送的流量元数据 - 用户态通过 ring buffer 批量读取,降低系统调用开销
- 支持零拷贝模式(
PerfEventArray+mmap)提升吞吐至 10M+ pps
2.3 XDP加速场景下的Go控制平面协同设计
在XDP数据面高速转发与Go控制平面之间,需构建低延迟、高一致性的协同机制。
数据同步机制
采用基于 eBPF map 的共享内存通道,配合 Go 的 github.com/cilium/ebpf 库实现原子更新:
// 将策略规则写入 pinned BPF map,供 XDP 程序实时查表
rule := PolicyRule{Action: 1, Proto: 6, DstPort: 443}
err := progMap.Update(uint32(0), rule, ebpf.UpdateAny)
if err != nil {
log.Fatal("map update failed:", err) // 错误需立即响应,避免控制-数据面状态撕裂
}
UpdateAny 允许覆盖已有键;uint32(0) 为固定策略槽位索引,确保 XDP 端单次查表完成决策。
协同架构概览
| 组件 | 职责 | 延迟敏感度 |
|---|---|---|
| XDP 程序 | 微秒级包过滤与重定向 | 极高 |
| Go 控制平面 | 规则编译、热加载、指标上报 | 中 |
| eBPF Map | 零拷贝跨上下文状态共享 | 高 |
graph TD
A[Go 控制平面] -->|ebpf.Map.Update| B[eBPF Map]
B --> C[XDP 程序]
C -->|bpf_map_lookup_elem| B
2.4 eBPF Map共享机制与Go应用状态同步实战
eBPF Map 是内核与用户空间协同的核心桥梁,支持多种类型(如 BPF_MAP_TYPE_HASH、BPF_MAP_TYPE_PERCPU_ARRAY),实现高效、无锁的状态共享。
数据同步机制
Go 程序通过 libbpf-go 打开并操作 eBPF Map,以原子方式读写键值对:
// 打开已加载的 map(名称需与 BPF C 端一致)
map, err := objMaps.Load("stats_map")
if err != nil {
log.Fatal(err)
}
// 写入统计键:CPU ID → 计数
cpuID := uint32(0)
count := uint64(1)
if err := map.Update(unsafe.Pointer(&cpuID), unsafe.Pointer(&count), 0); err != nil {
log.Fatal("Update failed:", err)
}
✅ Update() 第三参数 flags=0 表示覆盖写入;若用 BPF_ANY 可显式声明语义。
✅ 键/值内存布局须严格匹配 BPF C 端定义(如 struct stats_key),否则触发 -EINVAL。
常见 Map 类型对比
| 类型 | 多核安全 | 用户态并发访问 | 典型用途 |
|---|---|---|---|
HASH |
✅(内核级 RCU) | ✅(需自行加锁) | 动态连接追踪 |
PERCPU_ARRAY |
✅(每 CPU 独立副本) | ✅(零拷贝读取) | 高频计数聚合 |
同步流程示意
graph TD
A[Go 应用调用 map.Update] --> B[libbpf-go 封装 ioctl]
B --> C[内核 bpf_map_update_elem]
C --> D[原子哈希插入/更新]
D --> E[eBPF 程序实时读取同一 Map]
2.5 安全沙箱化eBPF加载与运行时验证方案
eBPF程序在内核中执行前,必须通过双重验证:静态校验器(verifier)与沙箱化加载策略协同保障安全性。
核心验证阶段
- 字节码合法性检查:禁止越界内存访问、无限循环、未初始化寄存器使用
- 权限最小化约束:基于
bpf_program__set_attach_target()显式声明所需辅助函数白名单 - 运行时上下文隔离:每个程序在独立
bpf_verifier_env中解析,避免跨程序状态污染
典型加载流程(mermaid)
graph TD
A[用户态bpf_obj_load] --> B[内核bpf_prog_load]
B --> C[Verifier逐指令模拟执行]
C --> D{是否通过所有安全规则?}
D -->|是| E[映射至特权受限的eBPF JIT区域]
D -->|否| F[拒绝加载并返回-EINVAL]
关键参数说明(表格)
| 参数 | 含义 | 安全影响 |
|---|---|---|
BPF_F_STRICT_ALIGNMENT |
强制4字节对齐访问 | 防止绕过边界检查 |
BPF_F_ANY_ALIGNMENT |
允许非对齐访问(仅限特定助手) | 需配合bpf_probe_read_*等安全读取接口 |
// 加载时启用严格验证与沙箱标志
int fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,
prog_insns, insns_cnt,
"GPL", 0, // license & log_level
NULL, 0); // no log buffer → 拒绝不安全路径
该调用隐式触发完整验证链:从指令集语义分析、寄存器生命周期跟踪,到辅助函数调用图可达性判定,确保零信任环境下的确定性执行。
第三章:Service Mesh适配型Go网络中间件
3.1 Sidecar透明劫持与Go Envoy xDS协议实现
Sidecar 模式通过 iptables 或 eBPF 实现流量透明重定向,使应用无感知地接入服务网格。Go 编写的控制平面需精准对接 Envoy 的 xDS v3 协议。
数据同步机制
Envoy 采用增量 xDS(DeltaDiscoveryRequest/Response)降低资源开销,支持版本控制(version_info)与资源监听(resource_names_subscribe)。
协议交互关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
node.id |
string | 唯一标识 Sidecar 实例 |
resource_names |
[]string | 请求订阅的 Cluster/Route 名称列表 |
response_nonce |
string | 服务端响应唯一性校验凭证 |
// 初始化 DeltaAggregatedResourcesServiceClient
client := pb.NewDeltaAggregatedResourcesServiceClient(conn)
stream, _ := client.DeltaAggregatedResources(ctx)
stream.Send(&pb.DeltaDiscoveryRequest{
Node: &core.Node{Id: "sidecar~10.1.2.3~app-v1~default.svc.cluster.local"},
TypeUrl: "type.googleapis.com/envoy.config.cluster.v3.Cluster",
ResourceNamesSubscribe: []string{"backend-cluster"},
VersionInfo: "1",
})
逻辑分析:Node.Id 需严格匹配 Envoy 启动时传入的 --service-node;TypeUrl 决定 xDS 资源类型;VersionInfo 触发幂等更新,避免重复推送。
graph TD
A[Go 控制平面] -->|DeltaDiscoveryRequest| B(Envoy Sidecar)
B -->|DeltaDiscoveryResponse| A
B -->|ACK/NACK 上报| A
3.2 Istio扩展点注入:Go编写的WASM Filter开发全流程
Istio 1.17+ 原生支持 Go 编写的 WebAssembly Filter,通过 proxy-wasm-go-sdk 实现零拷贝上下文访问。
环境准备
- 安装
tinygo(WASM 编译必需) - 初始化 SDK:
go get github.com/tetratelabs/proxy-wasm-go-sdk
核心过滤器结构
func NewPluginContext(contextID uint32) proxywasm.PluginContext {
return &myPluginContext{pluginContext: builtin.NewPluginContext()}
}
func (p *myPluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
// 解析配置、注册 HTTP 回调
proxywasm.SetHttpContext(p, p)
return types.OnPluginStartStatusOK
}
逻辑说明:
OnPluginStart在 Filter 加载时触发;pluginConfigurationSize表示 Envoy 传入的 YAML 配置字节数,需调用proxywasm.GetPluginConfiguration()读取并解析。
构建与部署流程
| 步骤 | 命令 | 说明 |
|---|---|---|
| 编译 | tinygo build -o filter.wasm -target=wasi ./main.go |
输出 WASM 模块,兼容 WASI ABI |
| 注入 | istioctl install --set profile=default --set meshConfig.defaultConfig.proxyMetadata.WASM_PLUGIN_URL=file:///var/lib/istio/envoy/filter.wasm |
通过元数据挂载本地文件 |
graph TD
A[Go源码] --> B[tinygo编译]
B --> C[WASM二进制]
C --> D[Sidecar注入]
D --> E[Envoy Runtime加载]
3.3 多集群服务发现同步:基于Go的gRPC-ADS协议轻量实现
核心设计目标
- 低延迟:端到端同步延迟
- 高保序:保障 EDS/CDS 更新的严格时序一致性
- 无状态:控制面不持久化服务实例,仅中转增量变更
数据同步机制
采用 gRPC 流式 ADS(Aggregated Discovery Service)单连接多资源订阅:
// 建立 ADS 流并注册资源类型
stream, err := client.StreamAggregatedResources(ctx)
if err != nil { /* handle */ }
stream.Send(&discovery.DiscoveryRequest{
TypeUrl: "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
ResourceNames: []string{"svc-payment", "svc-user"},
VersionInfo: "", // 初始空版本触发全量推送
})
逻辑分析:
StreamAggregatedResources复用单一 HTTP/2 连接,避免多 cluster 多连接带来的连接风暴;ResourceNames显式声明关注的服务名,实现按需同步;空VersionInfo触发首次全量快照,后续仅推送 delta。
同步状态映射表
| 资源类型 | 类型 URL | 版本字段 | 适用场景 |
|---|---|---|---|
| EDS | ...ClusterLoadAssignment |
version_info |
实例IP/权重动态更新 |
| CDS | ...Cluster |
version_info |
集群拓扑变更(如新增集群) |
流程概览
graph TD
A[多集群服务注册中心] -->|Watch 变更| B(ADS Server)
B -->|gRPC Stream| C[边缘网关集群1]
B -->|gRPC Stream| D[区域集群2]
B -->|gRPC Stream| E[灾备集群3]
第四章:WASM扩展驱动的Go网络运行时
4.1 WASI网络能力演进与Go+WASI-NN协同架构
WASI 网络能力从 wasi:sockets 到 wasi:http 的演进,显著提升了 WebAssembly 模块的网络语义表达力。Go 编译器通过 GOOS=wasi GOARCH=wasm 支持 WASI 运行时,天然兼容 wasi-sockets 接口。
WASI-NN 与网络协同关键路径
- Go 主程序加载
.wasm模块并注入wasi:nn实例 - WASI-NN 模块通过
wasi:sockets发起推理结果回传 - 网络调用经由 WASI host 提供的
sock_connect→sock_write→sock_close链路完成
Go 调用 WASI-NN 的核心代码片段
// 初始化 WASI-NN 实例(需预注册 wasi:nn 接口)
ctx := wasi.WithConfig(ctx, wasi.Config{
NN: &wasi.NNConfig{Backend: "ggml"},
})
inst, _ := wasm.NewInstance(wasmBytes, ctx)
// 触发推理并异步发起 HTTP 回调
inst.Exports["infer_and_notify"]()
此处
wasi.NNConfig.Backend="ggml"指定轻量级推理后端;infer_and_notify是导出函数,内部调用wasi:http/outgoing-handler发送结构化响应。
| 组件 | 版本要求 | 关键能力 |
|---|---|---|
wasi:sockets |
v0.2.0+ | TCP/UDP 基础连接 |
wasi:http |
v0.2.2+ | HTTP/1.1 请求构造 |
wasi:nn |
v0.2.1+ | 张量加载与推理调度 |
graph TD
A[Go Host] -->|invoke| B[WASI-NN Module]
B -->|wasi:sockets| C[WASI Host Network]
C -->|HTTP POST| D[Inference Gateway]
4.2 Bytecode Alliance WAPC标准在Go代理中的嵌入实践
WAPC(WebAssembly Portable Capability)是Bytecode Alliance提出的标准化宿主-模块通信协议,旨在解耦Wasm运行时与能力提供者。在Go语言构建的轻量代理中嵌入WAPC,可实现安全、可插拔的扩展逻辑。
核心集成步骤
- 初始化WAPC host runtime(如
wapc-go) - 注册 capability handler(如
http.invoke) - 加载
.wasm模块并绑定 guest call handler
能力注册示例
host := wapc.NewHost(
wapc.WithGuestCallHandler(func(binding, namespace, operation string, payload []byte) ([]byte, error) {
if namespace == "http" && operation == "invoke" {
return handleHTTP(payload) // 实现自定义HTTP转发
}
return nil, fmt.Errorf("unsupported op: %s/%s", namespace, operation)
}),
)
该代码注册统一入口处理所有guest发起的能力调用;binding标识调用来源(如 "wascc"),payload为序列化JSON请求体,返回值将被自动反序列化回Wasm内存。
| 字段 | 类型 | 说明 |
|---|---|---|
binding |
string | 宿主绑定标识,用于多运行时区分 |
namespace |
string | 能力命名空间(如 "keyvalue") |
operation |
string | 具体操作名(如 "get") |
graph TD
A[Go Proxy] -->|WAPC invoke| B[Wasm Guest]
B -->|http.invoke| C[Host Handler]
C --> D[HTTP Client]
D -->|Response| C
C -->|Serialized result| B
4.3 动态策略热加载:Go主机Runtime与WASM模块通信模型
WASM模块在策略引擎中需零停机更新,Go Runtime通过 wasmer 运行时实现内存隔离下的热替换。
数据同步机制
主机与WASM间通过线性内存共享策略配置结构体(含版本号、TTL、规则数组),避免序列化开销。
通信协议设计
- 主机调用
wasm_update_policy()导出函数触发重载 - WASM模块回调
host_notify_load_success()上报状态 - 版本号校验失败时自动回滚至前一有效快照
// Go侧策略热加载入口
func (r *Runtime) HotReload(policyBytes []byte) error {
// 1. 编译为新实例(不销毁旧实例)
newInst, err := r.compiler.CompileInstance(policyBytes)
if err != nil { return err }
// 2. 原子切换函数指针引用
atomic.StorePointer(&r.currentInst, unsafe.Pointer(newInst))
return nil
}
policyBytes 为 .wasm 二进制流;atomic.StorePointer 保证切换期间旧策略仍可安全执行;切换后首次调用即路由至新实例。
| 阶段 | 主机动作 | WASM响应 |
|---|---|---|
| 加载前 | 冻结策略匹配请求 | 暂停定时器 |
| 切换瞬间 | 原子更新实例指针 | 清空本地缓存 |
| 加载后 | 触发 on_policy_ready |
返回新版本号与规则数 |
graph TD
A[主机发起HotReload] --> B[编译新WASM实例]
B --> C{版本校验通过?}
C -->|是| D[原子切换currentInst]
C -->|否| E[保留旧实例,返回错误]
D --> F[通知WASM on_policy_ready]
4.4 性能压测对比:原生Go插件 vs WASM沙箱化扩展
压测环境配置
- CPU:8核 Intel Xeon E5-2680 v4
- 内存:32GB,无swap干扰
- 工具:
hey -n 100000 -c 200(HTTP负载)
核心性能指标(TPS & P99延迟)
| 方案 | 平均TPS | P99延迟(ms) | 内存增量(MB) |
|---|---|---|---|
| 原生Go插件 | 12,480 | 18.3 | +42 |
| WASM(Wazero) | 8,920 | 31.7 | +19 |
关键调用路径差异
// Go插件直接调用:零序列化开销
func (p *Plugin) Process(req *Request) *Response {
return p.handler(req) // 直接函数调用,<100ns
}
逻辑分析:无跨语言边界、无ABI转换;
p.handler为静态链接的Go函数指针,调用开销接近内联。参数req为堆分配对象复用,避免GC压力。
;; WASM导出函数签名(via Wazero)
(func $process (param $req_ptr i32) (result i32)
;; req_ptr指向线性内存中序列化后的JSON字节流
;; 需反序列化→业务处理→序列化响应 →额外~1.2ms开销
)
逻辑分析:每次调用需经Wazero runtime解码/编码,
$req_ptr指向WASM线性内存,与宿主Go内存隔离;i32返回值为响应偏移量,强制两次内存拷贝。
安全与弹性权衡
- WASM优势:进程级故障隔离、热更新无需重启、细粒度资源配额(CPU/内存)
- Go插件优势:极致吞吐、零序列化、可直接访问宿主runtime(如pprof、trace)
graph TD
A[HTTP请求] –> B{插件执行入口}
B –>|Go插件| C[直接调用handler]
B –>|WASM| D[序列化→WASM内存→执行→反序列化]
C –> E[低延迟高吞吐]
D –> F[高隔离低吞吐]
第五章:稀缺项目综合评估与选型指南
评估维度的实战锚点
在金融级区块链跨链网关项目选型中,团队曾对比Hyperledger Fabric v2.5、Corda 5.0与Quorum v23.10.0三个候选方案。关键锚点并非白皮书宣称的TPS,而是实际压测中合约升级停机时间(Fabric需重启Peer节点,平均47秒;Quorum通过私有交易通道实现零停机热更新)。该指标直接关联监管报送系统的SLA承诺(99.99%可用性),成为一票否决项。
多源验证的数据采集表
| 评估项 | Fabric v2.5 | Corda 5.0 | Quorum v23.10.0 | 验证方式 |
|---|---|---|---|---|
| 国密SM4支持 | 需第三方插件 | 原生不支持 | 内置国密套件 | 查阅源码crypto/sm4.go |
| 审计日志粒度 | 通道级 | 事务级 | 智能合约调用级 | 部署后抓取10万条日志分析 |
| K8s Operator成熟度 | v1.2(社区版) | 无官方支持 | v3.7(ConsenSys维护) | Helm chart版本及CI流水线覆盖率 |
供应链溯源项目的决策树
graph TD
A[是否需对接海关总署单一窗口API] -->|是| B[必须支持GB/T 38651-2020国标报文]
A -->|否| C[可接受ISO/IEC 15459编码]
B --> D[检查SDK是否内置海关加密证书加载器]
D -->|存在| E[进入性能测试阶段]
D -->|缺失| F[评估开发成本:平均增加127人时]
C --> G[优先测试ISO标准兼容性]
隐私计算场景的硬件依赖分析
某医疗影像联邦学习项目要求GPU显存≥24GB且支持NVIDIA TCC模式。实测发现:
- NVIDIA A100 40GB(PCIe版)在TCC模式下,TensorRT推理吞吐量达1,842 img/s
- 同规格A100(SXM版)因驱动限制无法启用TCC,吞吐量骤降至623 img/s
- AMD MI250X虽理论算力更高,但PyTorch 2.1.0未提供ROCm 6.0+的HIPBLAS优化,训练收敛速度慢4.3倍
开源许可证的合规红线
Apache 2.0许可的Kubeflow Pipelines允许修改后闭源分发,但若集成GPLv3的PostgreSQL扩展(如pg_cron),则整个调度系统必须开源。某券商AI平台因未识别此依赖链,在上线前安全审计中触发License冲突告警,导致延期17天重构任务编排模块。
生产环境灾备验证清单
- 主集群跨AZ部署时,etcd quorum丢失容忍阈值是否经混沌工程验证(如kill -9模拟3个节点宕机)
- 备份恢复RTO实测值:从S3快照拉起完整服务耗时(含证书重签、配置校验、健康检查)
- 网络策略变更后,Service Mesh中sidecar注入率是否保持100%(通过kubectl get pods -o jsonpath='{.items[*].metadata.annotations.sidecar.istio.io/status}’统计)
跨团队协作的接口契约
采用OpenAPI 3.1规范定义区块链节点管理接口,强制要求:
- 所有
/v1/nodes/{id}/restart请求必须携带X-Approval-ID头(值为OA审批单号) - 响应体中
status字段枚举值限定为{"pending","restarting","failed"},禁止返回"success"等模糊状态 - 错误码统一使用RFC 7807 Problem Details格式,
type字段必须指向内部知识库URL(如https://wiki.company.com/err/BC-007)
成本模型的动态测算
某CDN边缘计算项目采用Spot实例混合部署,通过Terraform模块实时计算:
locals {
spot_savings = (var.on_demand_price - var.spot_price) / var.on_demand_price * 100
recovery_risk = length(data.aws_spot_instance_requests.active.ids) < 3 ? 0.4 : 0
}
output "total_cost_per_hour" {
value = format("%.2f", var.base_cost * (1 - local.spot_savings/100) * (1 + local.recovery_risk))
} 