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

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上述架构的核心设计考量反亲和性调度GPU 推理节点必须分布在不同的物理宿主机上避免宿主机故障导致整个推理服务不可用。模型预加载备可用区的 GPU 节点需要保持模型的热加载状态确保故障切换时无需从头加载模型权重通常需要数分钟。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; ResponseEntityInfrastructureHealth 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; ResponseEntityModelLoadStatus 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 次探测请求取平均延迟 ListLong 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 ConcurrentHashMapString, NodeStatus nodes new ConcurrentHashMap(); private final AtomicReferenceSetString 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 ListString getAvailableNodes() { return nodes.values().stream() .filter(NodeStatus::isAvailable) .sorted(Comparator.comparingDouble(NodeStatus::getLoadScore)) .map(NodeStatus::getNodeId) .collect(Collectors.toList()); } private void refreshHealthyNodes() { SetString 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%。这意味着故障切换后用户可能察觉到回答质量的变化。解决方案是模型版本一致性管理确保所有 AZ 中的 GPU 节点加载完全相同的模型版本、相同的精度配置如都是 FP16 或都是 INT8、相同的推理参数如 Temperature、Top-P推理结果缓存对于高频问题如今天的天气如何通过语义缓存Semantic Cache返回已验证的答案避免切换到新节点后推理质量波动A/B 测试验证在故障切换后自动抽取 1% 的流量进行推理质量评估通过自动化评估模型或人工抽检如果发现质量下降超过阈值立即告警。5.3 混沌工程在 LLM 服务中的特殊考虑对传统微服务有效的混沌实验如随机杀进程、网络延迟注入在 LLM 服务中需要特别设计。我们增加了 LLM 特有的故障场景GPU 显存压力测试通过cuda-memcheck或自定义 CUDA 内核注入显存错误验证服务的错误处理和健康检查机制模型权重损坏模拟修改模型文件的部分字节如翻转一个权重文件的 1% 字节验证加载时的校验机制KV-Cache 溢出构造超长上下文请求如 100K tokens验证服务的降级策略如截断上下文或返回错误推理服务假死让推理服务进程存活但 GPU 利用率始终为 0模拟 GPU 驱动崩溃验证健康检查能否检测到这种僵尸状态。这些实验帮助我们发现并修复了多个隐蔽的故障模式显著提升了系统的鲁棒性。建议将混沌实验作为 LLM 服务上线前的必选流程并在每次模型版本升级后重新执行。六、总结LLM 服务的高可用架构设计需要在传统微服务高可用方案的基础上充分考虑 GPU 资源的特殊性。核心要点包括分层级的健康检查基础设施 → 模型加载 → 推理性能巡检、基于故障级别矩阵的差异化切换策略、以及网关层的动态节点注册机制。在实际落地中建议结合混沌工程持续验证故障切换的可靠性确保每一层冗余真正可工作。