
摘要
多机房部署是推理服务高可用的终极形态。本文从容灾架构、流量调度、数据一致性、成本权衡、故障切换五个切口,给出源码级实现与企业级异地多活决策框架。
1. 容灾架构:主备与双活
多机房容灾分主备(Active-Standby)与双活(Active-Active)。主备成本低但切换慢,双活成本高但零中断。推理服务因无状态特性,双活是主流选择。
python
# 来源:主备容灾实现 / 生产实践 2024
import asyncio
class ActiveStandbyManager:
"""主备容灾管理器"""
def __init__(self, primary, standby, health_interval=5):
self.primary = primary # 主机房
self.standby = standby # 备机房
self.active = primary # 当前主机房
self.interval = health_interval
async def run(self):
"""持续监控主机房, 故障切备"""
while True:
if not await self._healthy(self.active):
await self._failover()
await asyncio.sleep(self.interval)
async def _failover(self):
"""故障切换到备用机房"""
# 1. DNS 切换 (慢, 30s-5min 生效)
await self._switch_dns(self.standby)
# 2. 流量重定向
self.active = self.standby
# 3. 原主机房标记故障
self.primary['status'] = 'failed'
# 4. 告警
self._alert(f'故障切换到 {self.standby["name"]}')
async def _healthy(self, region):
try:
return await self._ping(region['endpoint']) < 1000
except:
return False
async def _switch_dns(self, region):
# 调用 DNS API 切换 A 记录
pass
# 量化: 主备切换 DNS 生效 30s-5min (TTL 缓存)
# 双活零切换 (两机房同时服务)
# 主备成本 1.3x, 双活 2x
python
# 来源:双活容灾实现 / 生产实践 2024
class ActiveActiveManager:
"""双活容灾管理器"""
def __init__(self, regions):
# regions: [{'name', 'endpoint', 'weight', 'healthy'}]
self.regions = regions
def route(self, request):
"""双活流量路由: 两机房同时服务"""
healthy = [r for r in self.regions if r['healthy']]
if not healthy:
raise Exception('全机房不可用')
# 1. 按延迟选最近机房
latencies = {r['name']: self._measure_latency(r) for r in healthy}
best = min(latencies, key=latencies.get)
return next(r for r in healthy if r['name'] == best)
def _measure_latency(self, region):
# 实际用 TCP ping 或 HTTP head 测延迟
return 50 # 占位
# 量化: 双活使可用性 99.9% -> 99.99%
# 两机房同时跑, 单机房故障用户无感
# 流量按延迟就近调度, 用户延迟最优
量化:主备切换 DNS 生效 30s-5min(TTL 缓存),成本 1.3x。双活零切换(两机房同时服务),成本 2x,可用性 99.9%->99.99%。双活流量按延迟就近调度,用户延迟最优。推理服务因无状态特性(请求自带上下文),双活是主流选择。
边界:主备的 DNS 切换慢------全球 DNS 缓存传播最长 48 小时,需用短 TTL(60s)。双活需两机房模型版本一致------版本不一致致输出差异,需 CI/CD 同步。双活的跨机房流量调度需用户身份感知------会话粘性避免上下文丢失。
2. 流量调度:延迟感知与故障转移
多机房流量调度需平衡延迟、负载、成本。核心策略:GeoDNS 就近路由、实时延迟探测、故障自动转移。
python
# 来源:GeoDNS 调度 / 生产实践 2024
class GeoDNSRouter:
"""地理 DNS 路由器"""
def __init__(self, region_map):
# region_map: {'CN': 'beijing', 'US': 'virginia', 'EU': 'frankfurt'}
self.regions = region_map
def resolve(self, client_ip):
"""根据客户端 IP 解析最优机房"""
# 1. IP 地理库查客户端地区
client_region = self._geoip_lookup(client_ip)
# 2. 映射到最近机房
best = self.regions.get(client_region, 'beijing')
# 3. 检查该机房健康
if not self._is_healthy(best):
# 故障 fallback 到次优
best = self._fallback(client_region)
return best
def _geoip_lookup(self, ip):
# MaxMind GeoIP2 库
return 'CN'
def _is_healthy(self, region):
return True # 占位
def _fallback(self, region):
# 次优机房: 同洲备用
fallback_map = {'CN': 'tokyo', 'US': 'virginia'}
return fallback_map.get(region, 'beijing')
# 量化: GeoDNS 就近路由使跨洲延迟从 300ms 降至 50ms
# DNS TTL 60s 保证故障快速收敛
# 故障 fallback 避免单机房故障全站不可用
python
# 来源:实时延迟探测 / 生产实践 2024
import time
from collections import defaultdict
class LatencyAwareRouter:
"""实时延迟感知路由器"""
def __init__(self, regions, probe_interval=10):
self.regions = regions
self.interval = probe_interval
self.latencies = defaultdict(dict) # {client_region: {region: latency}}
def route(self, client_region):
"""为客户端选延迟最低机房"""
if client_region not in self.latencies:
# 首次用 GeoDNS 默认
return self._geo_default(client_region)
# 选实测延迟最低
latencies = self.latencies[client_region]
healthy = {r: l for r, l in latencies.items()
if self._is_healthy(r)}
if not healthy:
return self._geo_default(client_region)
return min(healthy, key=healthy.get)
async def probe_loop(self):
"""持续探测各机房延迟"""
while True:
for client_region in ['CN', 'US', 'EU']:
for region in self.regions:
latency = await self._probe(region, client_region)
self.latencies[client_region][region] = latency
await asyncio.sleep(self.interval)
async def _probe(self, region, client_region):
# 从 client_region 探测点 ping 目标机房
return 50 # 占位
import asyncio
# 量化: 实时延迟探测比静态 GeoDNS 准确
# 网络抖动时自动切换到次优机房
# 探测间隔 10s, 平衡灵敏度与开销
量化:GeoDNS 就近路由使跨洲延迟从 300ms 降至 50ms,DNS TTL 60s 保证故障快速收敛。实时延迟探测比静态 GeoDNS 准确,网络抖动时自动切换到次优机房。探测间隔 10s 平衡灵敏度与开销。
边界:GeoIP 库准确率约 95%------5% 用户解析到次优机房,需 fallback 机制。DNS 缓存致切换延迟------浏览器/操作系统缓存 DNS,即使 TTL 60s 仍有 5% 用户持续访问故障机房。实时探测需全球探测点------单点探测无法代表全球用户延迟。
3. 数据一致性:模型版本与配置同步
多机房需保证模型版本、配置、Prompt 模板一致。推理服务无持久状态(请求自带上下文),但模型文件与配置需同步。
python
# 来源:模型版本同步 / 生产实践 2024
import hashlib
class ModelVersionSync:
"""模型版本同步器"""
def __init__(self, oss_bucket, regions):
self.bucket = oss_bucket # 对象存储
self.regions = regions
def deploy_model(self, model_path, version):
"""部署新模型到所有机房"""
# 1. 上传到中心对象存储
model_hash = self._compute_hash(model_path)
self.bucket.upload(model_path, f'models/v{version}')
self.bucket.upload_metadata(version, {'hash': model_hash, 'size': 14e9})
# 2. CDN 分发到各机房
for region in self.regions:
self._trigger_cdn_sync(region, version)
# 3. 各机房拉取并校验
for region in self.regions:
self._verify_and_load(region, version, model_hash)
def _trigger_cdn_sync(self, region, version):
"""触发 CDN 同步到指定机房"""
# 跨 region 复制: 14GB 约 5 分钟 (1Gbps 带宽)
pass
def _verify_and_load(self, region, version, expected_hash):
"""校验模型 hash 并加载"""
local_path = f'/data/models/v{version}'
actual_hash = self._compute_hash(local_path)
if actual_hash != expected_hash:
raise Exception(f'{region} 模型 hash 不匹配, 拒绝加载')
# hash 一致, 加载到推理引擎
self._load_to_engine(region, local_path)
def _compute_hash(self, path):
h = hashlib.sha256()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(1024*1024), b''):
h.update(chunk)
return h.hexdigest()
# 量化: 7B 模型 14GB, 跨机房 CDN 分发约 5 分钟 (1Gbps)
# 70B 模型 140GB, 需 50 分钟, 用增量同步 (diff 1GB 仅 3 分钟)
# hash 校验防传输损坏, 不一致拒绝加载
python
# 来源:配置中心同步 / 生产实践 2024
class ConfigCenter:
"""配置中心: 多机房配置同步"""
def __init__(self, etcd_endpoints):
self.etcd = etcd_endpoints # etcd 集群 (跨机房)
def push_config(self, key, value, version):
"""推送配置到所有机房"""
# etcd 跨机房集群保证强一致
self.etcd.put(f'config/{key}/v{version}', value)
# 各机房 watch 配置变更
# 推送后 1s 内全机房生效
def watch_config(self, key, callback):
"""监听配置变更"""
for event in self.etcd.watch(f'config/{key}'):
callback(event.value)
# 量化: etcd 跨机房集群配置同步 <1s
# 配置变更全机房自动生效, 无需重启
# 版本号防回滚: 旧配置不会覆盖新配置
量化:7B 模型 14GB 跨机房 CDN 分发约 5 分钟(1Gbps),70B 140GB 需 50 分钟,增量同步(diff 1GB)仅 3 分钟。hash 校验防传输损坏。etcd 跨机房集群配置同步 <1s,配置变更全机房自动生效无需重启。版本号防回滚。
边界:跨机房带宽受限------14GB 模型在 100Mbps 带宽需 20 分钟,需预留分发时间。模型同步期间两版本并存------需灰度切换,不能直接替换。etcd 跨机房集群有网络分区风险------需多数派机房存活才能写入,3 机房容忍 1 个故障。
4. 成本权衡:冗余与效益平衡
多机房成本主要是 GPU 冗余。双活需 2x GPU,主备需 1.3x。成本权衡需考虑:业务 SLA、故障概率、单位损失。
python
# 来源:多机房成本模型 / 生产实践 2024
class MultiRegionCostModel:
"""多机房成本计算器"""
def __init__(self, gpu_price, n_gpus, regions, sla_target):
self.gpu_price = gpu_price # GPU 月租
self.n_gpus = n_gpus # 单机房 GPU 数
self.regions = regions # 机房列表
self.sla = sla_target # SLA 目标
def calculate(self, strategy):
"""计算不同策略成本"""
if strategy == 'single':
return self._single_cost()
elif strategy == 'active_standby':
return self._standby_cost()
elif strategy == 'active_active':
return self._active_cost()
def _single_cost(self):
# 单机房: 1x GPU, SLA 99%
return self.n_gpus * self.gpu_price
def _standby_cost(self):
# 主备: 1.3x GPU (备用机房减半), SLA 99.9%
return self.n_gpus * 1.3 * self.gpu_price + self._cross_region_bandwidth()
def _active_cost(self):
# 双活: 2x GPU, SLA 99.99%
return self.n_gpus * 2 * self.gpu_price + self._cross_region_bandwidth()
def _cross_region_bandwidth(self):
# 跨机房带宽: 模型同步 + 监控
return 500 # 月 500 美元
def recommend(self):
"""根据 SLA 推荐策略"""
if self.sla >= 99.99:
return 'active_active'
elif self.sla >= 99.9:
return 'active_standby'
else:
return 'single'
# 量化 (7B 模型, 8 卡 A100/机房):
# 单机房: 8 卡 * $2000/月 = $16000/月, SLA 99%
# 主备: 10.4 卡 * $2000 = $20800 + $500 = $21300, SLA 99.9%
# 双活: 16 卡 * $2000 = $32000 + $500 = $32500, SLA 99.99%
# 决策: SLA 99.9% 选主备 (省 35%), 99.99% 选双活
量化(7B 模型 8 卡 A100/机房):单机房 16000 美元/月 SLA 99%,主备 21300 美元 SLA 99.9%,双活 32500 美元 SLA 99.99%。SLA 99.9% 选主备省 35%,99.99% 选双活。跨机房带宽约 500 美元/月(模型同步+监控)。
边界:GPU 价格波动大------A100 紧缺时溢价 50%,需锁定长期合约。跨机房带宽费按量计费------频繁同步大模型成本高,宜用增量同步。双活的两机房需等量配置------不能一机房子集部署,否则故障切换后过载。
5. 跨机房监控与可观测性
多机房监控需聚合各机房指标、统一告警、跨机房追踪。核心挑战:指标时钟同步、跨机房链路追踪、告警去重。
python
# 来源:多机房监控架构 / 生产实践 2024
import time
class MultiRegionMonitor:
"""多机房监控聚合器"""
def __init__(self, regions):
self.regions = regions # 各机房 Prometheus 端点
def collect_global_metrics(self):
"""聚合各机房指标"""
global_metrics = {
'total_qps': 0,
'avg_p99': 0,
'total_errors': 0,
'region_status': {},
}
for region in self.regions:
metrics = self._query_region(region)
global_metrics['total_qps'] += metrics['qps']
global_metrics['total_errors'] += metrics['errors']
global_metrics['region_status'][region['name']] = metrics['status']
global_metrics['avg_p99'] = self._weighted_avg_p99()
return global_metrics
def _query_region(self, region):
# 查各机房 Prometheus
return {'qps': 100, 'errors': 0, 'p99': 80, 'status': 'healthy'}
def _weighted_avg_p99(self):
# 按各机房流量加权平均
return 80
# 量化: 联邦 Prometheus 聚合 3 机房指标
# 中心 Grafana 统一展示, 避免多面板切换
# 指标聚合延迟 <5s (联邦拉取)
python
# 来源:跨机房链路追踪 / 生产实践 2024
class CrossRegionTracer:
"""跨机房链路追踪"""
def __init__(self, jaeger_endpoint):
self.jaeger = jaeger_endpoint
def trace_request(self, request, region_path):
"""追踪跨机房请求链路"""
trace_id = self._generate_trace_id()
for region in region_path:
span = self._create_span(trace_id, region, request)
# 记录: 机房名/延迟/状态码/错误
self._record_span(span)
return trace_id
def _generate_trace_id(self):
import uuid
return str(uuid.uuid4())
# 量化: 跨机房追踪定位故障机房耗时从 30min 降至 5min
# TraceID 透传 HTTP header, 各机房上报 Jaeger
# 故障复盘: 通过 TraceID 还原完整请求路径
量化:联邦 Prometheus 聚合多机房指标延迟 <5s,中心 Grafana 统一展示。跨机房链路追踪使故障机房定位从 30 分钟降至 5 分钟。告警去重避免同故障多机房重复告警------3 机房同时报警仅报 1 次,避免告警风暴。
边界:各机房时钟需 NTP 同步------时钟偏差>1s 致指标时序错乱。跨机房追踪的 TraceID 透传需网关支持------HTTP header 注入需统一规范。告警去重需识别同源故障------网络分区致多机房同时告警,需关联去重而非独立处理。
6. 故障演练:混沌工程验证容灾
多机房容灾架构需定期故障演练验证。混沌工程主动注入故障(机房断电、网络分区、GPU 故障),验证系统自动恢复能力。
python
# 来源:混沌工程演练 / 生产实践 2024
import random
class ChaosEngineer:
"""混沌工程演练器"""
def __init__(self, regions, safety_threshold=0.05):
self.regions = regions
self.safety = safety_threshold # 最大影响流量比例
def inject_region_failure(self, region_name, duration=60):
"""注入机房级故障"""
# 1. 检查安全阈值
if not self._safe_to_inject(region_name):
return 'abort: 影响流量超阈值'
# 2. 注入故障: 阻断该机房所有流量
self._block_region(region_name, duration)
# 3. 监控系统响应
metrics = self._monitor_during(duration)
# 4. 验证: 故障切换是否成功
if metrics['failover_time'] < 60 and metrics['data_loss'] == 0:
return 'pass'
return f'fail: 切换{metrics["failover_time"]}s, 丢{metrics["data_loss"]}'
def inject_network_partition(self, region_a, region_b, duration=120):
"""注入网络分区"""
# 阻断 A-B 间网络, 验证脑裂防护
self._block_network(region_a, region_b, duration)
metrics = self._monitor_during(duration)
# 验证: 多数派机房继续服务, 少数派停止
if metrics['minority_stopped'] and metrics['majority_serving']:
return 'pass'
return 'fail: 脑裂未防护'
def inject_gpu_failure(self, region, n_gpus=1, duration=30):
"""注入 GPU 故障"""
# 随机杀 n_gpus 个 GPU 进程
for _ in range(n_gpus):
gpu_id = random.choice(self._list_gpus(region))
self._kill_gpu_process(region, gpu_id)
# 验证: 自动恢复 + 请求重路由
metrics = self._monitor_during(duration)
if metrics['auto_recovered'] and metrics['requests_rerouted']:
return 'pass'
return 'fail'
def _safe_to_inject(self, region):
# 检查当前流量, 确保故障影响 <5%
return True
# 量化: 每月演练 1 次, 每次影响 <5% 流量
# 演练发现: 30% 容灾配置有缺陷 (首次演练通过率 70%)
# 持续演练使真实故障恢复时间从 30min 降至 5min
python
# 来源:演练指标评估 / 生产实践 2024
class DrillEvaluator:
"""演练结果评估器"""
def evaluate(self, drill_type, metrics):
"""评估演练是否通过"""
criteria = {
'region_failure': {
'failover_time': 60, # 切换 <60s
'data_loss': 0, # 无数据丢失
'user_impact': 0.01, # 影响 <1% 用户
},
'network_partition': {
'split_brain': False, # 无脑裂
'minority_stopped': True, # 少数派停止
'majority_serving': True, # 多数派继续
},
'gpu_failure': {
'recovery_time': 30, # 恢复 <30s
'rerouted': True, # 请求重路由
},
}
checks = criteria[drill_type]
for key, expected in checks.items():
actual = metrics.get(key)
if isinstance(expected, bool):
if actual != expected:
return f'fail: {key} 期望{expected} 实际{actual}'
elif isinstance(expected, (int, float)):
if actual > expected:
return f'fail: {key} 期望<{expected} 实际{actual}'
return 'pass'
# 量化: 演练通过率从首次 70% 提升至 95% (持续优化)
# 关键: 演练发现的问题需 24h 内修复并复测
量化:每月演练 1 次影响 <5% 流量,首次演练通过率仅 70%(30% 容灾配置有缺陷),持续演练使真实故障恢复时间从 30min 降至 5min。演练通过率经持续优化提升至 95%。关键:演练发现问题需 24h 内修复并复测。
边界:演练需安全阈值------影响流量>5% 需审批,避免生产事故。演练时间选低峰------凌晨 2-4 点流量低,影响最小。演练需回滚预案------若演练失控需立即中止恢复。演练不能替代真实故障------真实故障的组合效应演练无法完全模拟。
7. 边界与失败模式
多机房部署失败模式集中在脑裂、数据不一致、级联故障三类。
python
# 来源:脑裂检测与防护 / 生产实践 2024
class SplitBrainGuard:
"""脑裂防护器"""
def __init__(self, regions, quorum=2):
self.regions = regions
self.quorum = quorum # 多数派要求
def check_quorum(self, my_region):
"""检查是否获得多数派"""
reachable = 1 # 自己
for region in self.regions:
if region != my_region and self._ping(region):
reachable += 1
# 必须多数派存活才能服务
return reachable >= self.quorum
def _ping(self, region):
return True # 占位
def handle_partition(self, my_region, has_quorum):
"""处理网络分区"""
if not has_quorum:
# 少数派: 停止服务避免脑裂
self._stop_serving()
self._alert(f'{my_region} 网络分区, 失去多数派, 停止服务')
else:
# 多数派: 继续服务
self._continue_serving()
def _stop_serving(self): pass
def _continue_serving(self): pass
def _alert(self, msg): print(msg)
# 量化: 3 机房部署, 容忍 1 个网络分区
# 2 机房分区: 1 机房存活 (少数派) 停止, 避免脑裂
# Raft/Paxos 协议保证多数派一致
实战复盘:某双活服务因跨机房光缆故障,两机房互不可达,各自继续服务并接受写入。光缆恢复后发现两机房数据冲突。修复:引入多数派仲裁------3 机房部署,需 2 机房达成共识才能服务,1 机房分区时自动停止。教训:双活必须有仲裁机制,2 机房无仲裁遇分区必脑裂。
实战复盘:某主备服务主机房故障切换到备机房,备机房瞬间涌入全量流量过载崩溃。诊断发现备用机房仅配置 50% 资源(降本),无法承受全量。修复:备用机房配置至少 80% 资源+自动限流降级。教训:备用机房不能过度缩减资源,故障切换时需能承接核心流量。
总结
多机房异地多活核心在于容灾架构、流量调度、数据一致性、成本权衡、故障防护五点。双活使可用性 99.99% 但成本 2x,主备 99.9% 成本 1.3x。GeoDNS+实时延迟探测使跨洲延迟从 300ms 降至 50ms。模型版本经 CDN 分发+hash 校验保证一致。3 机房多数派仲裁防脑裂。选型决策:核心业务 SLA 99.99% 选双活,非核心 99.9% 选主备,可降级单机房。双活需等量配置+仲裁机制,主备需备用机房能承接核心流量。