LLM 服务的高可用架构——多 AZ 部署、故障转移与流量切换

📅 2026/7/19 0:35:04 👁️ 阅读次数 📝 编程学习
LLM 服务的高可用架构——多 AZ 部署、故障转移与流量切换

LLM 服务的高可用架构——多 AZ 部署、故障转移与流量切换

一、背景与挑战

在企业级 AI 应用中,LLM(大语言模型)推理服务的可用性直接影响业务连续性。与传统的无状态微服务不同,LLM 推理服务具有 GPU 资源密集、推理延迟高、模型加载耗时长等特征,这使得其高可用架构设计面临独特的挑战。

核心问题包括:单点 GPU 集群故障导致服务中断、推理请求的长连接特性使得故障转移窗口更敏感、模型分片(Tensor Parallelism / Pipeline Parallelism)下部分节点失效的级联影响、以及跨可用区(AZ)部署时的 GPU 资源调度与成本平衡。

本文将从工程实践角度,系统梳理 LLM 服务高可用架构的设计要点,涵盖多 AZ 部署策略、故障自动检测与转移、流量切换机制三个核心维度。

二、多 AZ 部署架构设计

在云原生环境下,跨可用区部署是实现高可用的基础手段。对于 LLM 推理服务而言,核心架构如下:

graph TB subgraph "全局流量调度层" DNS[DNS 智能解析] GSLB[全局负载均衡 GSLB] end subgraph "AZ-1(主可用区)" LB1[负载均衡器] GW1[AI 网关集群] MGR1[模型管理服务] GPU1[GPU 推理集群<br/>3×8×A100] CACHE1[KV-Cache 共享存储] end subgraph "AZ-2(备可用区)" LB2[负载均衡器] GW2[AI 网关集群] MGR2[模型管理服务] GPU2[GPU 推理集群<br/>3×8×A100] CACHE2[KV-Cache 共享存储] end subgraph "共享服务层" MQ[(消息队列 Kafka)] DB[(元数据库)] REG[服务注册中心] end DNS --> GSLB GSLB --> LB1 GSLB --> LB2 LB1 --> GW1 --> MGR1 --> GPU1 LB2 --> GW2 --> MGR2 --> GPU2 GPU1 -.-> CACHE1 GPU2 -.-> CACHE2 MGR1 --> DB MGR2 --> DB GW1 --> MQ GW2 --> MQ MGR1 --> REG MGR2 --> REG

上述架构的核心设计考量:

  1. 反亲和性调度:GPU 推理节点必须分布在不同的物理宿主机上,避免宿主机故障导致整个推理服务不可用。
  2. 模型预加载:备可用区的 GPU 节点需要保持模型的热加载状态,确保故障切换时无需从头加载模型权重(通常需要数分钟)。
  3. KV-Cache 独立:每个 AZ 维护独立的 KV-Cache 存储,避免跨 AZ 的延迟开销影响推理性能。

三、故障检测与健康检查的工程实现

LLM 推理节点的健康检查需要同时关注基础设施层和应用层。以下是一个综合健康检查的 Java 实现:

/** * GPU 推理节点健康检查器 * 同时检测 GPU 可用性、模型加载状态和推理延迟 */ @Component public class GpuNodeHealthChecker { private static final Logger log = LoggerFactory.getLogger(GpuNodeHealthChecker.class); /** 推理延迟阈值(毫秒),超过此值视作不健康 */ private static final long INFERENCE_LATENCY_THRESHOLD_MS = 5000; /** 健康检查采样间隔 */ private static final long CHECK_INTERVAL_SECONDS = 5; private final RestTemplate restTemplate; private final HealthMetricRepository metricRepository; public GpuNodeHealthChecker(RestTemplate restTemplate, HealthMetricRepository metricRepository) { this.restTemplate = restTemplate; this.metricRepository = metricRepository; } /** * 对指定 GPU 节点执行综合健康检查 * * @param nodeEndpoint GPU 推理节点地址 * @return 健康状态结果 */ public HealthCheckResult performHealthCheck(String nodeEndpoint) { HealthCheckResult result = new HealthCheckResult(); result.setNodeId(nodeEndpoint); result.setCheckTimestamp(System.currentTimeMillis()); try { // 第一层:基础设施健康检查(GPU 显存、温度、功耗) InfrastructureHealth infraHealth = checkInfrastructure(nodeEndpoint); result.setInfrastructureHealthy(infraHealth.isHealthy()); if (!infraHealth.isHealthy()) { log.warn("GPU 节点 {} 基础设施异常:{}", nodeEndpoint, infraHealth.getDetail()); result.setStatus(HealthStatus.UNHEALTHY); return result; } // 第二层:模型加载状态检查 ModelLoadStatus loadStatus = checkModelLoadStatus(nodeEndpoint); result.setModelLoaded(loadStatus.isLoaded()); if (!loadStatus.isLoaded()) { log.warn("GPU 节点 {} 模型未加载或异常:{}", nodeEndpoint, loadStatus.getMessage()); result.setStatus(HealthStatus.UNHEALTHY); return result; } // 第三层:推理性能检查(发送探测请求,测量延迟) InferenceLatency latency = checkInferenceLatency(nodeEndpoint); result.setInferenceLatencyMs(latency.getAverageMs()); if (latency.getAverageMs() > INFERENCE_LATENCY_THRESHOLD_MS) { log.warn("GPU 节点 {} 推理延迟过高:{}ms,超过阈值 {}ms", nodeEndpoint, latency.getAverageMs(), INFERENCE_LATENCY_THRESHOLD_MS); result.setStatus(HealthStatus.DEGRADED); } else { result.setStatus(HealthStatus.HEALTHY); } // 持久化健康指标 metricRepository.save(result); } catch (RestClientException e) { log.error("GPU 节点 {} 健康检查网络异常", nodeEndpoint, e); result.setStatus(HealthStatus.UNREACHABLE); } catch (Exception e) { log.error("GPU 节点 {} 健康检查未知异常", nodeEndpoint, e); result.setStatus(HealthStatus.UNKNOWN); } return result; } /** * 检查 GPU 基础设施状态(显存使用率、温度等) */ private InfrastructureHealth checkInfrastructure(String nodeEndpoint) { try { String url = nodeEndpoint + "/v1/health/gpu"; ResponseEntity<InfrastructureHealth> response = restTemplate.getForEntity(url, InfrastructureHealth.class); return response.getBody(); } catch (RestClientException e) { log.error("基础设施检查失败:{}", nodeEndpoint, e); return InfrastructureHealth.unhealthy("网络不可达:" + e.getMessage()); } } /** * 检查模型是否在目标节点上正确加载 */ private ModelLoadStatus checkModelLoadStatus(String nodeEndpoint) { try { String url = nodeEndpoint + "/v1/health/model"; ResponseEntity<ModelLoadStatus> response = restTemplate.getForEntity(url, ModelLoadStatus.class); return response.getBody(); } catch (RestClientException e) { log.error("模型加载状态检查失败:{}", nodeEndpoint, e); return ModelLoadStatus.notLoaded("检查请求失败:" + e.getMessage()); } } /** * 通过发送轻量探测请求测量推理延迟 */ private InferenceLatency checkInferenceLatency(String nodeEndpoint) { String url = nodeEndpoint + "/v1/completions/probe"; ProbeRequest probeRequest = new ProbeRequest("ping", 1); // 发送 3 次探测请求,取平均延迟 List<Long> latencies = new ArrayList<>(); for (int i = 0; i < 3; i++) { long start = System.currentTimeMillis(); try { restTemplate.postForEntity(url, probeRequest, Void.class); latencies.add(System.currentTimeMillis() - start); } catch (RestClientException e) { log.error("推理延迟探测失败(第{}次):{}", i + 1, nodeEndpoint, e); latencies.add(INFERENCE_LATENCY_THRESHOLD_MS * 2); } } return InferenceLatency.of(latencies); } }

四、故障转移与流量切换策略

故障转移的核心难点在于 LLM 推理请求通常承载着长上下文(可能达数十 K tokens),简单重试会导致 Context 丢失。推荐的分层切换策略如下:

切换决策矩阵:

故障级别触发条件切换策略切换延迟
L1-节点级单 GPU 节点不可用同 AZ 内其他节点接管< 5 秒
L2-集群级单 AZ 内 GPU 集群整体异常自动切换至备 AZ< 30 秒
L3-可用区级整个 AZ 不可用GSLB 层DNS切换< 60 秒
L4-全局级所有 AZ 异常降级至缓存兜底或排队即时

在实现层面,流量切换通过 AI 网关完成。网关维护一个动态的 Nodes Registry,基于健康检查结果实时更新节点状态:

/** * GPU 节点注册中心——基于健康检查结果动态管理可用节点 */ @Service public class DynamicNodeRegistry { private final ConcurrentHashMap<String, NodeStatus> nodes = new ConcurrentHashMap<>(); private final AtomicReference<Set<String>> healthyNodes = new AtomicReference<>(Collections.emptySet()); /** * 根据健康检查结果更新节点状态 * 连续 3 次不健康才标记为不可用,避免误判 */ public void updateNodeHealth(String nodeId, HealthCheckResult result) { NodeStatus status = nodes.computeIfAbsent(nodeId, k -> new NodeStatus(nodeId)); if (result.getStatus() == HealthStatus.HEALTHY || result.getStatus() == HealthStatus.DEGRADED) { status.recordSuccess(); if (status.getConsecutiveFailures() >= 3) { log.info("GPU 节点 {} 从故障中恢复", nodeId); } status.resetFailures(); } else { status.recordFailure(); if (status.getConsecutiveFailures() >= 3) { log.error("GPU 节点 {} 连续 {} 次故障,标记为不可用", nodeId, status.getConsecutiveFailures()); status.setAvailable(false); refreshHealthyNodes(); } } } /** * 获取当前可用的健康节点列表 */ public List<String> getAvailableNodes() { return nodes.values().stream() .filter(NodeStatus::isAvailable) .sorted(Comparator.comparingDouble(NodeStatus::getLoadScore)) .map(NodeStatus::getNodeId) .collect(Collectors.toList()); } private void refreshHealthyNodes() { Set<String> healthy = nodes.values().stream() .filter(NodeStatus::isAvailable) .map(NodeStatus::getNodeId) .collect(Collectors.toSet()); healthyNodes.set(healthy); } }

五、成本优化与推理质量边界

5.1 多 AZ 部署的资源成本分析

多 AZ 部署虽然能提升可用性,但带来了显著的 GPU 资源成本。在我们的实践中,双 AZ 部署意味着需要预留双倍的 GPU 资源(主 AZ 100% 容量 + 备 AZ 100% 容量),因为备 AZ 需要随时能接管全部流量。对于大模型推理服务(如 8×A100 节点),这意味着数千万的额外硬件投入。

成本优化的方案是部分冗余:备 AZ 只预留 50% 的容量,当主 AZ 故障时,通过降级策略(如关闭非核心功能、降低并发限制)来应对。在我们的实践中,这种方案能将资源成本降低约 35%,同时将 RTO(恢复时间目标)控制在 5 分钟以内。

此外,对于开发/测试环境,可以使用定时启停策略:备 AZ 的 GPU 节点在非工作时间(如晚上 10 点至早上 8 点)自动缩容到 0,仅保留 API 网关层用于告警。这种策略能将非生产环境的 GPU 成本降低约 60%。

5.2 故障切换时的推理质量一致性

LLM 推理服务的一个独特挑战是:不同 GPU 节点上的模型推理结果可能存在细微差异(由于浮点计算的非确定性、CUDA 版本的细微差别)。在我们的观测中,同一个 Prompt 在 A100 和 H100 上的推理结果,BLEU 分数差异可达 2-3%。

这意味着,故障切换后用户可能察觉到回答质量的变化。解决方案是:

  1. 模型版本一致性管理:确保所有 AZ 中的 GPU 节点加载完全相同的模型版本、相同的精度配置(如都是 FP16 或都是 INT8)、相同的推理参数(如 Temperature、Top-P);
  2. 推理结果缓存:对于高频问题(如"今天的天气如何"),通过语义缓存(Semantic Cache)返回已验证的答案,避免切换到新节点后推理质量波动;
  3. A/B 测试验证:在故障切换后,自动抽取 1% 的流量进行推理质量评估(通过自动化评估模型或人工抽检),如果发现质量下降超过阈值,立即告警。

5.3 混沌工程在 LLM 服务中的特殊考虑

对传统微服务有效的混沌实验(如随机杀进程、网络延迟注入),在 LLM 服务中需要特别设计。我们增加了 LLM 特有的故障场景:

  1. GPU 显存压力测试:通过cuda-memcheck或自定义 CUDA 内核注入显存错误,验证服务的错误处理和健康检查机制;
  2. 模型权重损坏模拟:修改模型文件的部分字节(如翻转一个权重文件的 1% 字节),验证加载时的校验机制;
  3. KV-Cache 溢出:构造超长上下文请求(如 100K tokens),验证服务的降级策略(如截断上下文或返回错误);
  4. 推理服务"假死":让推理服务进程存活但 GPU 利用率始终为 0(模拟 GPU 驱动崩溃),验证健康检查能否检测到这种"僵尸"状态。

这些实验帮助我们发现并修复了多个隐蔽的故障模式,显著提升了系统的鲁棒性。建议将混沌实验作为 LLM 服务上线前的必选流程,并在每次模型版本升级后重新执行。

六、总结

LLM 服务的高可用架构设计需要在传统微服务高可用方案的基础上,充分考虑 GPU 资源的特殊性。核心要点包括:分层级的健康检查(基础设施 → 模型加载 → 推理性能巡检)、基于故障级别矩阵的差异化切换策略、以及网关层的动态节点注册机制。在实际落地中,建议结合混沌工程持续验证故障切换的可靠性,确保每一层冗余真正可工作。